Improvement: SSL offloading with Virtual Router (#11468)

* SSL offloading with Virtual Router

* PR11468: fix pre-commit errors

* PR11468: api->getAPI/postAPI in UI

* SSL: add smoke tests for VPC in user project

* PR11468: address Daan's comments

* Fix test/integration/smoke/test_ssl_offloading.py

* SSL: remove ssl certificates when clean up account

* SSL offloading: add unit tests

* SSL offloading: UI fixes part 1

* SSL offloading: UI changes part 2

* SSL offloading: add more unit tests

* SSL offloading: more unit tests 3

* SSL offloading: wrong check

* SSL offloading: more and more unit tests

* SSL offloading: add testUpdateLoadBalancerRule5
This commit is contained in:
Wei Zhou 2025-09-11 13:07:18 +02:00 committed by GitHub
parent 8089d32740
commit b46e29dc67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 2080 additions and 100 deletions

View File

@ -48,6 +48,7 @@ repos:
exclude: > exclude: >
(?x) (?x)
^scripts/vm/systemvm/id_rsa\.cloud$| ^scripts/vm/systemvm/id_rsa\.cloud$|
^server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java$|
^server/src/test/java/com/cloud/keystore/KeystoreTest\.java$| ^server/src/test/java/com/cloud/keystore/KeystoreTest\.java$|
^server/src/test/resources/certs/dsa_self_signed\.key$| ^server/src/test/resources/certs/dsa_self_signed\.key$|
^server/src/test/resources/certs/non_root\.key$| ^server/src/test/resources/certs/non_root\.key$|
@ -57,7 +58,8 @@ repos:
^server/src/test/resources/certs/rsa_self_signed\.key$| ^server/src/test/resources/certs/rsa_self_signed\.key$|
^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$| ^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$|
^systemvm/agent/certs/localhost\.key$| ^systemvm/agent/certs/localhost\.key$|
^systemvm/agent/certs/realhostip\.key$ ^systemvm/agent/certs/realhostip\.key$|
^test/integration/smoke/test_ssl_offloading.py$
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: \.vhd$ exclude: \.vhd$
- id: fix-byte-order-marker - id: fix-byte-order-marker
@ -75,7 +77,7 @@ repos:
name: run codespell name: run codespell
description: Check spelling with codespell description: Check spelling with codespell
args: [--ignore-words=.github/linters/codespell.txt] args: [--ignore-words=.github/linters/codespell.txt]
exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$ exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$|^server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java$|^test/integration/smoke/test_ssl_offloading.py$
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: 7.0.0 rev: 7.0.0
hooks: hooks:

View File

@ -71,7 +71,7 @@ public class LoadBalancerTO {
this.destinations = new DestinationTO[destinations.size()]; this.destinations = new DestinationTO[destinations.size()];
this.stickinessPolicies = null; this.stickinessPolicies = null;
this.sslCert = null; this.sslCert = null;
this.lbProtocol = null; this.lbProtocol = protocol;
int i = 0; int i = 0;
for (LbDestination destination : destinations) { for (LbDestination destination : destinations) {
this.destinations[i++] = new DestinationTO(destination.getIpAddress(), destination.getDestinationPortStart(), destination.isRevoked(), false); this.destinations[i++] = new DestinationTO(destination.getIpAddress(), destination.getDestinationPortStart(), destination.isRevoked(), false);
@ -205,6 +205,10 @@ public class LoadBalancerTO {
return this.sslCert; return this.sslCert;
} }
public void setLbSslCert(LbSslCert sslCert) {
this.sslCert = sslCert;
}
public String getSrcIpVlan() { public String getSrcIpVlan() {
return srcIpVlan; return srcIpVlan;
} }

View File

@ -106,7 +106,7 @@ public interface LoadBalancingRulesService {
boolean applyLoadBalancerConfig(long lbRuleId) throws ResourceUnavailableException; boolean applyLoadBalancerConfig(long lbRuleId) throws ResourceUnavailableException;
boolean assignCertToLoadBalancer(long lbRuleId, Long certId); boolean assignCertToLoadBalancer(long lbRuleId, Long certId, boolean isForced);
boolean removeCertFromLoadBalancer(long lbRuleId); boolean removeCertFromLoadBalancer(long lbRuleId);

View File

@ -27,6 +27,7 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.FirewallRuleResponse; import org.apache.cloudstack.api.response.FirewallRuleResponse;
import org.apache.cloudstack.api.response.SslCertResponse; import org.apache.cloudstack.api.response.SslCertResponse;
import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.commons.lang3.BooleanUtils;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.ConcurrentOperationException;
@ -57,11 +58,17 @@ public class AssignCertToLoadBalancerCmd extends BaseAsyncCmd {
description = "the ID of the certificate") description = "the ID of the certificate")
Long certId; Long certId;
@Parameter(name = ApiConstants.FORCED,
type = CommandType.BOOLEAN,
since = "4.22",
description = "Force assign the certificate. If there is a certificate assigned to the LB, it will be removed at first.")
private Boolean forced;
@Override @Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
ResourceAllocationException, NetworkRuleConflictException { ResourceAllocationException, NetworkRuleConflictException {
//To change body of implemented methods use File | Settings | File Templates. //To change body of implemented methods use File | Settings | File Templates.
if (_lbService.assignCertToLoadBalancer(getLbRuleId(), getCertId())) { if (_lbService.assignCertToLoadBalancer(getLbRuleId(), getCertId(), isForced())) {
SuccessResponse response = new SuccessResponse(getCommandName()); SuccessResponse response = new SuccessResponse(getCommandName());
this.setResponseObject(response); this.setResponseObject(response);
} else { } else {
@ -95,4 +102,19 @@ public class AssignCertToLoadBalancerCmd extends BaseAsyncCmd {
public Long getLbRuleId() { public Long getLbRuleId() {
return lbRuleId; return lbRuleId;
} }
public boolean isForced() {
return BooleanUtils.toBoolean(forced);
}
@Override
public String getSyncObjType() {
return BaseAsyncCmd.networkSyncObject;
}
@Override
public Long getSyncObjId() {
LoadBalancer lb = _entityMgr.findById(LoadBalancer.class, getLbRuleId());
return (lb != null)? lb.getNetworkId(): null;
}
} }

View File

@ -33,6 +33,7 @@ import org.apache.cloudstack.api.response.LoadBalancerResponse;
import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.NetworkResponse;
import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;
import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType; import com.cloud.dc.DataCenter.NetworkType;
@ -112,7 +113,7 @@ public class CreateLoadBalancerRuleCmd extends BaseAsyncCreateCmd /*implements L
+ "rule will be created for. Required when public Ip address is not associated with any Guest network yet (VPC case)") + "rule will be created for. Required when public Ip address is not associated with any Guest network yet (VPC case)")
private Long networkId; private Long networkId;
@Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "The protocol for the LB such as tcp, udp or tcp-proxy.") @Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "The protocol for the LB such as tcp, udp, tcp-proxy or ssl.")
private String lbProtocol; private String lbProtocol;
@Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the rule to the end user or not", since = "4.4", authorized = {RoleType.Admin}) @Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the rule to the end user or not", since = "4.4", authorized = {RoleType.Admin})
@ -253,7 +254,7 @@ public class CreateLoadBalancerRuleCmd extends BaseAsyncCreateCmd /*implements L
} }
public String getLbProtocol() { public String getLbProtocol() {
return lbProtocol; return StringUtils.trim(StringUtils.lowerCase(lbProtocol));
} }
///////////////////////////////////////////////////// /////////////////////////////////////////////////////

View File

@ -82,4 +82,15 @@ public class RemoveCertFromLoadBalancerCmd extends BaseAsyncCmd {
public Long getLbRuleId() { public Long getLbRuleId() {
return this.lbRuleId; return this.lbRuleId;
} }
@Override
public String getSyncObjType() {
return BaseAsyncCmd.networkSyncObject;
}
@Override
public Long getSyncObjId() {
LoadBalancer lb = _entityMgr.findById(LoadBalancer.class, getLbRuleId());
return (lb != null)? lb.getNetworkId(): null;
}
} }

View File

@ -56,6 +56,8 @@ public class LoadBalancerConfigItem extends AbstractConfigItemFacade {
final String[] statRules = allRules[LoadBalancerConfigurator.STATS]; final String[] statRules = allRules[LoadBalancerConfigurator.STATS];
final LoadBalancerRule loadBalancerRule = new LoadBalancerRule(configuration, tmpCfgFilePath, tmpCfgFileName, addRules, removeRules, statRules, routerIp); final LoadBalancerRule loadBalancerRule = new LoadBalancerRule(configuration, tmpCfgFilePath, tmpCfgFileName, addRules, removeRules, statRules, routerIp);
final LoadBalancerRule.SslCertEntry[] sslCerts = cfgtr.generateSslCertEntries(command);
loadBalancerRule.setSslCerts(sslCerts);
final List<LoadBalancerRule> rules = new LinkedList<LoadBalancerRule>(); final List<LoadBalancerRule> rules = new LinkedList<LoadBalancerRule>();
rules.add(loadBalancerRule); rules.add(loadBalancerRule);

View File

@ -25,6 +25,7 @@ public class LoadBalancerRule {
private String[] configuration; private String[] configuration;
private String tmpCfgFilePath; private String tmpCfgFilePath;
private String tmpCfgFileName; private String tmpCfgFileName;
private SslCertEntry[] sslCerts;
private String[] addRules; private String[] addRules;
private String[] removeRules; private String[] removeRules;
@ -32,6 +33,53 @@ public class LoadBalancerRule {
private String routerIp; private String routerIp;
public static class SslCertEntry {
private String name;
private String cert;
private String key;
private String chain;
private String password;
public SslCertEntry(String name, String cert, String key, String chain, String password) {
this.name = name;
this.cert = cert;
this.key = key;
this.chain = chain;
this.password = password;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setCert(String cert) {
this.cert = cert;
}
public String getCert() {
return cert;
}
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public void setChain(String chain) {
this.chain = chain;
}
public String getChain() {
return chain;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
public LoadBalancerRule() { public LoadBalancerRule() {
// Empty constructor for (de)serialization // Empty constructor for (de)serialization
} }
@ -101,4 +149,12 @@ public class LoadBalancerRule {
public void setRouterIp(final String routerIp) { public void setRouterIp(final String routerIp) {
this.routerIp = routerIp; this.routerIp = routerIp;
} }
public SslCertEntry[] getSslCerts() {
return sslCerts;
}
public void setSslCerts(final SslCertEntry[] sslCerts) {
this.sslCerts = sslCerts;
}
} }

View File

@ -36,6 +36,8 @@ import com.cloud.agent.api.to.LoadBalancerTO;
import com.cloud.agent.api.to.LoadBalancerTO.DestinationTO; import com.cloud.agent.api.to.LoadBalancerTO.DestinationTO;
import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO; import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO;
import com.cloud.agent.api.to.PortForwardingRuleTO; import com.cloud.agent.api.to.PortForwardingRuleTO;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType; import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType;
import com.cloud.utils.Pair; import com.cloud.utils.Pair;
import com.cloud.utils.net.NetUtils; import com.cloud.utils.net.NetUtils;
@ -52,6 +54,12 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
private static String[] defaultListen = {"listen vmops", "\tbind 0.0.0.0:9", "\toption transparent"}; private static String[] defaultListen = {"listen vmops", "\tbind 0.0.0.0:9", "\toption transparent"};
private static final String SSL_CERTS_DIR = "/etc/cloudstack/ssl/";
private static final String SSL_CONFIGURATION_INTERMEDIATE = " ssl-min-ver TLSv1.2 no-tls-tickets " +
"ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-GCM-SHA256 " +
"ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256";
@Override @Override
public String[] generateConfiguration(final List<PortForwardingRuleTO> fwRules) { public String[] generateConfiguration(final List<PortForwardingRuleTO> fwRules) {
// Group the rules by publicip:publicport // Group the rules by publicip:publicport
@ -469,30 +477,41 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
return sb.toString(); return sb.toString();
} }
private List<String> getRulesForPool(final LoadBalancerTO lbTO, final boolean keepAliveEnabled) { private List<String> getRulesForPool(final LoadBalancerTO lbTO, final LoadBalancerConfigCommand lbCmd) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
final String poolName = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString(); final String poolName = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString();
final String publicIP = lbTO.getSrcIp(); final String publicIP = lbTO.getSrcIp();
final int publicPort = lbTO.getSrcPort(); final int publicPort = lbTO.getSrcPort();
final String algorithm = lbTO.getAlgorithm(); final String algorithm = lbTO.getAlgorithm();
final List<String> result = new ArrayList<String>(); boolean sslOffloading = lbTO.getSslCert() != null && !lbTO.getSslCert().isRevoked()
// add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80" && NetUtils.SSL_PROTO.equals(lbTO.getLbProtocol());
sb = new StringBuilder();
sb.append("listen ").append(poolName); final List<String> frontendConfigs = new ArrayList<>();
result.add(sb.toString()); final List<String> backendConfigs = new ArrayList<>();
final List<String> result = new ArrayList<>();
sb = new StringBuilder(); sb = new StringBuilder();
sb.append("\tbind ").append(publicIP).append(":").append(publicPort); sb.append("\tbind ").append(publicIP).append(":").append(publicPort);
result.add(sb.toString());
if (sslOffloading) {
sb.append(" ssl crt ").append(SSL_CERTS_DIR).append(poolName).append(".pem");
// check for http2 support
sb.append(" alpn h2,http/1.1");
sb.append(SSL_CONFIGURATION_INTERMEDIATE);
sb.append("\n\thttp-request add-header X-Forwarded-Proto https");
}
frontendConfigs.add(sb.toString());
sb = new StringBuilder(); sb = new StringBuilder();
sb.append("\t").append("balance ").append(algorithm.toLowerCase()); sb.append("\t").append("balance ").append(algorithm.toLowerCase());
result.add(sb.toString()); backendConfigs.add(sb.toString());
int i = 0; int i = 0;
Boolean destsAvailable = false; boolean destsAvailable = false;
final String stickinessSubRule = getLbSubRuleForStickiness(lbTO); final String stickinessSubRule = getLbSubRuleForStickiness(lbTO);
final List<String> dstSubRule = new ArrayList<String>(); final List<String> dstSubRule = new ArrayList<>();
final List<String> dstWithCookieSubRule = new ArrayList<String>(); final List<String> dstWithCookieSubRule = new ArrayList<>();
for (final DestinationTO dest : lbTO.getDestinations()) { for (final DestinationTO dest : lbTO.getDestinations()) {
// add line like this: "server 65_37_141_30-80_3 10.1.1.4:80 check" // add line like this: "server 65_37_141_30-80_3 10.1.1.4:80 check"
if (dest.isRevoked()) { if (dest.isRevoked()) {
@ -500,15 +519,20 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
} }
sb = new StringBuilder(); sb = new StringBuilder();
sb.append("\t") sb.append("\t")
.append("server ") .append("server ")
.append(poolName) .append(poolName)
.append("_") .append("_")
.append(Integer.toString(i++)) .append(i++)
.append(" ") .append(" ")
.append(dest.getDestIp()) .append(dest.getDestIp())
.append(":") .append(":")
.append(dest.getDestPort()) .append(dest.getDestPort())
.append(" check"); .append(" check");
if (sslOffloading) {
sb.append(SSL_CONFIGURATION_INTERMEDIATE);
}
if(lbTO.getLbProtocol() != null && lbTO.getLbProtocol().equals("tcp-proxy")) { if(lbTO.getLbProtocol() != null && lbTO.getLbProtocol().equals("tcp-proxy")) {
sb.append(" send-proxy"); sb.append(" send-proxy");
} }
@ -520,9 +544,9 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
destsAvailable = true; destsAvailable = true;
} }
Boolean httpbasedStickiness = false; boolean httpbasedStickiness = false;
/* attach stickiness sub rule only if the destinations are available */ /* attach stickiness sub rule only if the destinations are available */
if (stickinessSubRule != null && destsAvailable == true) { if (stickinessSubRule != null && destsAvailable) {
for (final StickinessPolicyTO stickinessPolicy : lbTO.getStickinessPolicies()) { for (final StickinessPolicyTO stickinessPolicy : lbTO.getStickinessPolicies()) {
if (stickinessPolicy == null) { if (stickinessPolicy == null) {
continue; continue;
@ -530,35 +554,40 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName()) || if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName()) ||
StickinessMethodType.AppCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName())) { StickinessMethodType.AppCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName())) {
httpbasedStickiness = true; httpbasedStickiness = true;
break;
} }
} }
if (httpbasedStickiness) { if (httpbasedStickiness) {
result.addAll(dstWithCookieSubRule); backendConfigs.addAll(dstWithCookieSubRule);
} else { } else {
result.addAll(dstSubRule); backendConfigs.addAll(dstSubRule);
} }
result.add(stickinessSubRule); backendConfigs.add(stickinessSubRule);
} else { } else {
result.addAll(dstSubRule); backendConfigs.addAll(dstSubRule);
} }
if (stickinessSubRule != null && !destsAvailable) { if (stickinessSubRule != null && !destsAvailable) {
logger.warn("Haproxy stickiness policy for lb rule: " + lbTO.getSrcIp() + ":" + lbTO.getSrcPort() + ": Not Applied, cause: backends are unavailable"); logger.warn("Haproxy stickiness policy for lb rule: " + lbTO.getSrcIp() + ":" + lbTO.getSrcPort() + ": Not Applied, cause: backends are unavailable");
} }
if (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled || httpbasedStickiness) { boolean keepAliveEnabled = lbCmd.keepAliveEnabled;
sb = new StringBuilder(); boolean http = (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled);
sb.append("\t").append("mode http"); if (http || httpbasedStickiness || sslOffloading) {
result.add(sb.toString()); frontendConfigs.add("\tmode http");
sb = new StringBuilder(); String keepAliveLine = keepAliveEnabled ? "\tno option forceclose" : "\toption httpclose";
sb.append("\t").append("option httpclose"); frontendConfigs.add(keepAliveLine);
result.add(sb.toString());
} }
// add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80"
result.add(String.format("listen %s", poolName));
result.addAll(frontendConfigs);
String cidrList = lbTO.getCidrList(); String cidrList = lbTO.getCidrList();
if (StringUtils.isNotBlank(cidrList)) { if (StringUtils.isNotBlank(cidrList)) {
result.add(String.format("\tacl network_allowed src %s \n\ttcp-request connection reject if !network_allowed", cidrList)); result.add(String.format("\tacl network_allowed src %s \n\ttcp-request connection reject if !network_allowed", cidrList));
} }
result.addAll(backendConfigs);
result.add(blankLine); result.add(blankLine);
return result; return result;
} }
@ -566,15 +595,18 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
private String generateStatsRule(final LoadBalancerConfigCommand lbCmd, final String ruleName, final String statsIp) { private String generateStatsRule(final LoadBalancerConfigCommand lbCmd, final String ruleName, final String statsIp) {
final StringBuilder rule = new StringBuilder("\nlisten ").append(ruleName).append("\n\tbind ").append(statsIp).append(":").append(lbCmd.lbStatsPort); final StringBuilder rule = new StringBuilder("\nlisten ").append(ruleName).append("\n\tbind ").append(statsIp).append(":").append(lbCmd.lbStatsPort);
// TODO DH: write test for this in both cases // TODO DH: write test for this in both cases
if (!lbCmd.keepAliveEnabled) { rule.append("\n\tmode http");
logger.info("Haproxy mode http enabled"); if (lbCmd.keepAliveEnabled) {
rule.append("\n\tmode http\n\toption httpclose"); logger.info("Haproxy option http-keep-alive enabled");
} else {
logger.info("Haproxy option httpclose enabled");
rule.append("\n\toption httpclose");
} }
rule.append("\n\tstats enable\n\tstats uri ") rule.append("\n\tstats enable\n\tstats uri ")
.append(lbCmd.lbStatsUri) .append(lbCmd.lbStatsUri)
.append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ") .append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ")
.append(lbCmd.lbStatsAuth); .append(lbCmd.lbStatsAuth)
rule.append("\n"); .append("\n");
final String result = rule.toString(); final String result = rule.toString();
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Haproxystats rule: " + result); logger.debug("Haproxystats rule: " + result);
@ -644,7 +676,7 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
if (lbTO.isRevoked()) { if (lbTO.isRevoked()) {
continue; continue;
} }
final List<String> poolRules = getRulesForPool(lbTO, lbCmd.keepAliveEnabled); final List<String> poolRules = getRulesForPool(lbTO, lbCmd);
result.addAll(poolRules); result.addAll(poolRules);
has_listener = true; has_listener = true;
} }
@ -696,4 +728,30 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
return result; return result;
} }
@Override
public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd) {
final Set<SslCertEntry> sslCertEntries = new HashSet<>();
for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
if (lbTO.getSslCert() != null) {
addSslCertEntry(sslCertEntries, lbTO);
}
}
final SslCertEntry[] result = sslCertEntries.toArray(new SslCertEntry[sslCertEntries.size()]);
return result;
}
private void addSslCertEntry(Set<SslCertEntry> sslCertEntries, LoadBalancerTO lbTO) {
final LbSslCert cert = lbTO.getSslCert();
if (cert.isRevoked()) {
return;
}
if (lbTO.getLbProtocol() == null || ! lbTO.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
return;
}
StringBuilder sb = new StringBuilder();
final String name = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString();
final SslCertEntry sslCertEntry = new SslCertEntry(name, cert.getCert(), cert.getKey(), cert.getChain(), cert.getPassword());
sslCertEntries.add(sslCertEntry);
}
} }

View File

@ -23,6 +23,7 @@ import java.util.List;
import com.cloud.agent.api.routing.LoadBalancerConfigCommand; import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.to.PortForwardingRuleTO; import com.cloud.agent.api.to.PortForwardingRuleTO;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
public interface LoadBalancerConfigurator { public interface LoadBalancerConfigurator {
public final static int ADD = 0; public final static int ADD = 0;
@ -34,4 +35,6 @@ public interface LoadBalancerConfigurator {
public String[] generateConfiguration(LoadBalancerConfigCommand lbCmd); public String[] generateConfiguration(LoadBalancerConfigCommand lbCmd);
public String[][] generateFwRules(LoadBalancerConfigCommand lbCmd); public String[][] generateFwRules(LoadBalancerConfigCommand lbCmd);
public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd);
} }

View File

@ -57,6 +57,7 @@ import com.cloud.agent.resource.virtualnetwork.model.IpAssociation;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule; import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRules; import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRules;
import com.cloud.network.lb.LoadBalancingRule.LbDestination; import com.cloud.network.lb.LoadBalancingRule.LbDestination;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.Networks.TrafficType; import com.cloud.network.Networks.TrafficType;
public class ConfigHelperTest { public class ConfigHelperTest {
@ -223,9 +224,12 @@ public class ConfigHelperTest {
protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() { protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() {
final List<LoadBalancerTO> lbs = new ArrayList<>(); final List<LoadBalancerTO> lbs = new ArrayList<>();
final List<LbDestination> dests = new ArrayList<>(); final List<LbDestination> dests = new ArrayList<>();
final LbSslCert lbSslCert = new LbSslCert("cert", "key", "password", "chain", "fingerprint", false);
dests.add(new LbDestination(80, 8080, "10.1.10.2", false)); dests.add(new LbDestination(80, 8080, "10.1.10.2", false));
dests.add(new LbDestination(80, 8080, "10.1.10.2", true)); dests.add(new LbDestination(80, 8080, "10.1.10.2", true));
lbs.add(new LoadBalancerTO(UUID.randomUUID().toString(), "64.10.1.10", 80, "tcp", "algo", false, false, false, dests)); LoadBalancerTO loadBalancerTO = new LoadBalancerTO(UUID.randomUUID().toString(), "64.10.1.10", 80, "tcp", "algo", false, false, false, dests);
loadBalancerTO.setLbSslCert(lbSslCert);
lbs.add(loadBalancerTO);
final LoadBalancerTO[] arrayLbs = new LoadBalancerTO[lbs.size()]; final LoadBalancerTO[] arrayLbs = new LoadBalancerTO[lbs.size()];
lbs.toArray(arrayLbs); lbs.toArray(arrayLbs);

View File

@ -0,0 +1,63 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.agent.resource.virtualnetwork.model;
import org.junit.Assert;
import org.junit.Test;
public class LoadBalancerRuleTest {
@Test
public void testSslCertEntry() {
String name = "name";
String cert = "cert";
String key = "key1";
String chain = "chain";
String password = "password";
LoadBalancerRule.SslCertEntry sslCertEntry = new LoadBalancerRule.SslCertEntry(name, cert, key, chain, password);
Assert.assertEquals(name, sslCertEntry.getName());
Assert.assertEquals(cert, sslCertEntry.getCert());
Assert.assertEquals(key, sslCertEntry.getKey());
Assert.assertEquals(chain, sslCertEntry.getChain());
Assert.assertEquals(password, sslCertEntry.getPassword());
String name2 = "name2";
String cert2 = "cert2";
String key2 = "key2";
String chain2 = "chain2";
String password2 = "password2";
sslCertEntry.setName(name2);
sslCertEntry.setCert(cert2);
sslCertEntry.setKey(key2);
sslCertEntry.setChain(chain2);
sslCertEntry.setPassword(password2);
Assert.assertEquals(name2, sslCertEntry.getName());
Assert.assertEquals(cert2, sslCertEntry.getCert());
Assert.assertEquals(key2, sslCertEntry.getKey());
Assert.assertEquals(chain2, sslCertEntry.getChain());
Assert.assertEquals(password2, sslCertEntry.getPassword());
LoadBalancerRule loadBalancerRule = new LoadBalancerRule();
loadBalancerRule.setSslCerts(new LoadBalancerRule.SslCertEntry[]{sslCertEntry});
Assert.assertEquals(1, loadBalancerRule.getSslCerts().length);
Assert.assertEquals(sslCertEntry, loadBalancerRule.getSslCerts()[0]);
}
}

View File

@ -31,6 +31,7 @@ import org.junit.Test;
import com.cloud.agent.api.routing.LoadBalancerConfigCommand; import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.to.LoadBalancerTO; import com.cloud.agent.api.to.LoadBalancerTO;
import com.cloud.network.lb.LoadBalancingRule.LbDestination; import com.cloud.network.lb.LoadBalancingRule.LbDestination;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
@ -80,11 +81,11 @@ public class HAProxyConfiguratorTest {
HAProxyConfigurator hpg = new HAProxyConfigurator(); HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false); LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
String result = genConfig(hpg, cmd); String result = genConfig(hpg, cmd);
assertTrue("keepalive disabled should result in 'mode http' in the resulting haproxy config", result.contains("mode http")); assertTrue("keepalive disabled should result in 'option httpclose' in the resulting haproxy config", result.contains("\toption httpclose"));
cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true); cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true);
result = genConfig(hpg, cmd); result = genConfig(hpg, cmd);
assertTrue("keepalive enabled should not result in 'mode http' in the resulting haproxy config", !result.contains("mode http")); assertTrue("keepalive enabled should result in 'no option httpclose' in the resulting haproxy config", result.contains("\tno option httpclose"));
// TODO // TODO
// create lb command // create lb command
// setup tests for // setup tests for
@ -122,6 +123,19 @@ public class HAProxyConfiguratorTest {
Assert.assertTrue(result.contains("acl network_allowed src 1.1.1.1 2.2.2.2/24 \n\ttcp-request connection reject if !network_allowed")); Assert.assertTrue(result.contains("acl network_allowed src 1.1.1.1 2.2.2.2/24 \n\ttcp-request connection reject if !network_allowed"));
} }
@Test
public void generateConfigurationTestWithSslCert() {
LoadBalancerTO lb = new LoadBalancerTO("1", "10.2.0.1", 443, "ssl", "roundrobin", false, false, false, null);
final LbSslCert lbSslCert = new LbSslCert("cert", "key", "password", "chain", "fingerprint", false);
lb.setLbSslCert(lbSslCert);
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
String result = genConfig(hpg, cmd);
Assert.assertTrue(result.contains("bind 10.2.0.1:443 ssl crt /etc/cloudstack/ssl/10_2_0_1-443.pem"));
}
private String genConfig(HAProxyConfigurator hpg, LoadBalancerConfigCommand cmd) { private String genConfig(HAProxyConfigurator hpg, LoadBalancerConfigCommand cmd) {
String[] sa = hpg.generateConfiguration(cmd); String[] sa = hpg.generateConfiguration(cmd);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@ -22,4 +22,6 @@ import com.cloud.utils.db.GenericDao;
public interface SslCertDao extends GenericDao<SslCertVO, Long> { public interface SslCertDao extends GenericDao<SslCertVO, Long> {
List<SslCertVO> listByAccountId(Long id); List<SslCertVO> listByAccountId(Long id);
int removeByAccountId(long accountId);
} }

View File

@ -40,4 +40,10 @@ public class SslCertDaoImpl extends GenericDaoBase<SslCertVO, Long> implements S
return listBy(sc); return listBy(sc);
} }
@Override
public int removeByAccountId(long accountId) {
SearchCriteria<SslCertVO> sc = listByAccountId.create();
sc.setParameters("accountId", accountId);
return remove(sc);
}
} }

View File

@ -517,9 +517,11 @@ NetworkMigrationResponder, AggregatedCommandExecutor, RedundantResource, DnsServ
final Map<Capability, String> lbCapabilities = new HashMap<Capability, String>(); final Map<Capability, String> lbCapabilities = new HashMap<Capability, String>();
lbCapabilities.put(Capability.SupportedLBAlgorithms, "roundrobin,leastconn,source"); lbCapabilities.put(Capability.SupportedLBAlgorithms, "roundrobin,leastconn,source");
lbCapabilities.put(Capability.SupportedLBIsolation, "dedicated"); lbCapabilities.put(Capability.SupportedLBIsolation, "dedicated");
lbCapabilities.put(Capability.SupportedProtocols, "tcp, udp, tcp-proxy"); lbCapabilities.put(Capability.SupportedProtocols, "tcp, udp, tcp-proxy, ssl");
lbCapabilities.put(Capability.SupportedStickinessMethods, getHAProxyStickinessCapability()); lbCapabilities.put(Capability.SupportedStickinessMethods, getHAProxyStickinessCapability());
lbCapabilities.put(Capability.LbSchemes, LoadBalancerContainer.Scheme.Public.toString()); lbCapabilities.put(Capability.LbSchemes, LoadBalancerContainer.Scheme.Public.toString());
// Supports SSL offloading
lbCapabilities.put(Capability.SslTermination, "true");
// specifies that LB rules can support autoscaling and the list of // specifies that LB rules can support autoscaling and the list of
// counters it supports // counters it supports

View File

@ -1267,10 +1267,10 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
@Override @Override
@DB @DB
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_ASSIGN, eventDescription = "assigning certificate to load balancer", async = true) @ActionEvent(eventType = EventTypes.EVENT_LB_CERT_ASSIGN, eventDescription = "assigning certificate to load balancer", async = true)
public boolean assignCertToLoadBalancer(long lbRuleId, Long certId) { public boolean assignCertToLoadBalancer(long lbRuleId, Long certId, boolean forced) {
CallContext caller = CallContext.current(); CallContext caller = CallContext.current();
LoadBalancerVO loadBalancer = _lbDao.findById(Long.valueOf(lbRuleId)); LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
if (loadBalancer == null) { if (loadBalancer == null) {
throw new InvalidParameterValueException("Invalid load balancer id: " + lbRuleId); throw new InvalidParameterValueException("Invalid load balancer id: " + lbRuleId);
} }
@ -1292,10 +1292,7 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
throw new InvalidParameterValueException("Ssl termination not supported by the loadbalancer"); throw new InvalidParameterValueException("Ssl termination not supported by the loadbalancer");
} }
//check if the lb is already bound validateCertMapRule(lbRuleId, forced);
LoadBalancerCertMapVO certMapRule = _lbCertMapDao.findByLbRuleId(loadBalancer.getId());
if (certMapRule != null)
throw new InvalidParameterValueException("Another certificate is already bound to the LB");
//check for correct port //check for correct port
if (loadBalancer.getLbProtocol() == null || !(loadBalancer.getLbProtocol().equals(NetUtils.SSL_PROTO))) if (loadBalancer.getLbProtocol() == null || !(loadBalancer.getLbProtocol().equals(NetUtils.SSL_PROTO)))
@ -1326,6 +1323,18 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
return success; return success;
} }
private void validateCertMapRule(long lbRuleId, boolean forced) {
//check if the lb is already bound
LoadBalancerCertMapVO certMapRule = _lbCertMapDao.findByLbRuleId(lbRuleId);
if (certMapRule != null) {
if (!forced) {
throw new InvalidParameterValueException("Another certificate is already bound to the LB");
}
logger.debug("Another certificate is already bound to the LB, removing it");
removeCertFromLoadBalancer(lbRuleId);
}
}
@Override @Override
@DB @DB
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_REMOVE, eventDescription = "removing certificate from load balancer", async = true) @ActionEvent(eventType = EventTypes.EVENT_LB_CERT_REMOVE, eventDescription = "removing certificate from load balancer", async = true)
@ -1987,7 +1996,7 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
return handled; return handled;
} }
private LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) { protected LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) {
List<LbStickinessPolicy> policyList = getStickinessPolicies(lb.getId()); List<LbStickinessPolicy> policyList = getStickinessPolicies(lb.getId());
Ip sourceIp = getSourceIp(lb); Ip sourceIp = getSourceIp(lb);
@ -2257,12 +2266,17 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
LoadBalancerVO tmplbVo = _lbDao.findById(lbRuleId); LoadBalancerVO tmplbVo = _lbDao.findById(lbRuleId);
boolean success = _lbDao.update(lbRuleId, lb); boolean success = _lbDao.update(lbRuleId, lb);
// If algorithm is changed, have to reapply the lb config // If algorithm or lb protocol is changed, have to reapply the lb config
if ((algorithm != null) && (tmplbVo.getAlgorithm().compareTo(algorithm) != 0)){ boolean needToReApplyRule = (algorithm != null && !algorithm.equals(tmplbVo.getAlgorithm()))
|| (lbProtocol != null && !lbProtocol.equals(tmplbVo.getLbProtocol()));
if (needToReApplyRule) {
try { try {
lb.setState(FirewallRule.State.Add); lb.setState(FirewallRule.State.Add);
_lbDao.persist(lb); _lbDao.persist(lb);
applyLoadBalancerConfig(lbRuleId); applyLoadBalancerConfig(lbRuleId);
if (!lb.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
removeCertMapIfExists(lb);
}
} catch (ResourceUnavailableException e) { } catch (ResourceUnavailableException e) {
if (isRollBackAllowedForProvider(lb)) { if (isRollBackAllowedForProvider(lb)) {
/* /*
@ -2279,6 +2293,9 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
if (lbBackup.getAlgorithm() != null) { if (lbBackup.getAlgorithm() != null) {
lb.setAlgorithm(lbBackup.getAlgorithm()); lb.setAlgorithm(lbBackup.getAlgorithm());
} }
if (lbBackup.getLbProtocol() != null) {
lb.setLbProtocol(lbBackup.getLbProtocol());
}
lb.setState(lbBackup.getState()); lb.setState(lbBackup.getState());
_lbDao.update(lb.getId(), lb); _lbDao.update(lb.getId(), lb);
_lbDao.persist(lb); _lbDao.persist(lb);
@ -2309,6 +2326,14 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
} }
} }
private void removeCertMapIfExists(LoadBalancerVO lb) {
LoadBalancerCertMapVO loadBalancerCertMapVO = _lbCertMapDao.findByLbRuleId(lb.getId());
if (loadBalancerCertMapVO != null) {
logger.debug("Removing SSL cert for load balancer %s as the new protocol is not ssl but %s", lb, lb.getLbProtocol());
_lbCertMapDao.remove(loadBalancerCertMapVO.getId());
}
}
@Override @Override
public Pair<List<? extends UserVm>, List<String>> listLoadBalancerInstances(ListLoadBalancerRuleInstancesCmd cmd) throws PermissionDeniedException { public Pair<List<? extends UserVm>, List<String>> listLoadBalancerInstances(ListLoadBalancerRuleInstancesCmd cmd) throws PermissionDeniedException {
Account caller = CallContext.current().getCallingAccount(); Account caller = CallContext.current().getCallingAccount();

View File

@ -366,6 +366,7 @@ public class CommandSetupHelper {
final LoadBalancerTO lb = new LoadBalancerTO(uuid, srcIp, srcPort, protocol, algorithm, revoked, false, inline, destinations, stickinessPolicies); final LoadBalancerTO lb = new LoadBalancerTO(uuid, srcIp, srcPort, protocol, algorithm, revoked, false, inline, destinations, stickinessPolicies);
lb.setCidrList(rule.getCidrList()); lb.setCidrList(rule.getCidrList());
lb.setLbProtocol(lb_protocol); lb.setLbProtocol(lb_protocol);
lb.setLbSslCert(rule.getLbSslCert());
lbs[i++] = lb; lbs[i++] = lb;
} }
String routerPublicIp = null; String routerPublicIp = null;

View File

@ -929,6 +929,8 @@ public class NetworkHelperImpl implements NetworkHelper {
return false; return false;
} }
validateHAproxyLbProtocol(rule.getLbProtocol());
for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) { for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) {
final List<Pair<String, String>> paramsList = stickinessPolicy.getParams(); final List<Pair<String, String>> paramsList = stickinessPolicy.getParams();
@ -982,6 +984,13 @@ public class NetworkHelperImpl implements NetworkHelper {
return true; return true;
} }
private void validateHAproxyLbProtocol(String lbProtocol) {
List<String> lbProtocols = Arrays.asList("tcp", "udp", "tcp-proxy", "ssl");
if (lbProtocol != null && !lbProtocols.contains(lbProtocol)) {
throw new InvalidParameterValueException(String.format("protocol %s is not in valid protocols %s", lbProtocol, lbProtocols));
}
}
/* /*
* This function detects numbers like 12 ,32h ,42m .. etc,. 1) plain number * This function detects numbers like 12 ,32h ,42m .. etc,. 1) plain number
* like 12 2) time or tablesize like 12h, 34m, 45k, 54m , here last * like 12 2) time or tablesize like 12h, 34m, 45k, 54m , here last

View File

@ -1737,11 +1737,13 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
.append(",sourcePortEnd=").append(firewallRuleVO.getSourcePortEnd()); .append(",sourcePortEnd=").append(firewallRuleVO.getSourcePortEnd());
if (firewallRuleVO instanceof LoadBalancerVO) { if (firewallRuleVO instanceof LoadBalancerVO) {
LoadBalancerVO loadBalancerVO = (LoadBalancerVO) firewallRuleVO; LoadBalancerVO loadBalancerVO = (LoadBalancerVO) firewallRuleVO;
loadBalancingData.append(",sourceIp=").append(_ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString()) String sourceIp = _ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString();
loadBalancingData.append(",sourceIp=").append(sourceIp)
.append(",destPortStart=").append(loadBalancerVO.getDefaultPortStart()) .append(",destPortStart=").append(loadBalancerVO.getDefaultPortStart())
.append(",destPortEnd=").append(loadBalancerVO.getDefaultPortEnd()) .append(",destPortEnd=").append(loadBalancerVO.getDefaultPortEnd())
.append(",algorithm=").append(loadBalancerVO.getAlgorithm()) .append(",algorithm=").append(loadBalancerVO.getAlgorithm())
.append(",protocol=").append(loadBalancerVO.getLbProtocol()); .append(",protocol=").append(loadBalancerVO.getLbProtocol());
updateWithLbRuleSslCertificates(loadBalancingData, loadBalancerVO, sourceIp);
} else if (firewallRuleVO instanceof ApplicationLoadBalancerRuleVO) { } else if (firewallRuleVO instanceof ApplicationLoadBalancerRuleVO) {
ApplicationLoadBalancerRuleVO appLoadBalancerVO = (ApplicationLoadBalancerRuleVO) firewallRuleVO; ApplicationLoadBalancerRuleVO appLoadBalancerVO = (ApplicationLoadBalancerRuleVO) firewallRuleVO;
loadBalancingData.append(",sourceIp=").append(appLoadBalancerVO.getSourceIp()) loadBalancingData.append(",sourceIp=").append(appLoadBalancerVO.getSourceIp())
@ -1760,6 +1762,16 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
} }
} }
protected void updateWithLbRuleSslCertificates(final StringBuilder loadBalancingData, LoadBalancerVO loadBalancerVO, String sourceIp) {
if (NetUtils.SSL_PROTO.equals(loadBalancerVO.getLbProtocol())) {
final LbSslCert sslCert = _lbMgr.getLbSslCert(loadBalancerVO.getId());
if (sslCert != null && ! sslCert.isRevoked()) {
loadBalancingData.append(",sslcert=").append(sourceIp.replace(".", "_")).append('-')
.append(loadBalancerVO.getSourcePortStart()).append(".pem");
}
}
}
protected Map<String, String> getRouterHealthChecksConfig(final DomainRouterVO router) { protected Map<String, String> getRouterHealthChecksConfig(final DomainRouterVO router) {
Map<String, String> data = new HashMap<>(); Map<String, String> data = new HashMap<>();
List<DomainRouterJoinVO> routerJoinVOs = domainRouterJoinDao.searchByIds(router.getId()); List<DomainRouterJoinVO> routerJoinVOs = domainRouterJoinDao.searchByIds(router.getId());

View File

@ -136,6 +136,7 @@ import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.RemoteAccessVpnDao; import com.cloud.network.dao.RemoteAccessVpnDao;
import com.cloud.network.dao.RemoteAccessVpnVO; import com.cloud.network.dao.RemoteAccessVpnVO;
import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao; import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.router.VirtualRouter; import com.cloud.network.router.VirtualRouter;
import com.cloud.network.security.SecurityGroupManager; import com.cloud.network.security.SecurityGroupManager;
@ -309,6 +310,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
private UserDataDao userDataDao; private UserDataDao userDataDao;
@Inject @Inject
private NetworkPermissionDao networkPermissionDao; private NetworkPermissionDao networkPermissionDao;
@Inject
private SslCertDao sslCertDao;
private List<QuerySelector> _querySelectors; private List<QuerySelector> _querySelectors;
@ -1203,6 +1206,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
// Delete registered UserData // Delete registered UserData
userDataDao.removeByAccountId(accountId); userDataDao.removeByAccountId(accountId);
// Delete SSL certificates
sslCertDao.removeByAccountId(accountId);
// Delete Webhooks // Delete Webhooks
deleteWebhooksForAccount(accountId); deleteWebhooksForAccount(accountId);

View File

@ -26,8 +26,9 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore; import java.security.cert.CertStore;
@ -48,10 +49,6 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.acl.SecurityChecker;
@ -62,9 +59,21 @@ import org.apache.cloudstack.api.response.SslCertResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.network.tls.CertService; import org.apache.cloudstack.network.tls.CertService;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
@ -89,7 +98,6 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.security.CertificateHelper; import com.cloud.utils.security.CertificateHelper;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class CertServiceImpl implements CertService { public class CertServiceImpl implements CertService {
@ -279,11 +287,11 @@ public class CertServiceImpl implements CertService {
return certResponseList; return certResponseList;
} }
private void validate(final String certInput, final String keyInput, final String password, final String chainInput, boolean revocationEnabled) { protected void validate(final String certInput, final String keyInput, final String password, final String chainInput, boolean revocationEnabled) {
try { try {
List<Certificate> chain = null; List<Certificate> chain = null;
final Certificate cert = parseCertificate(certInput); final Certificate cert = parseCertificate(certInput);
final PrivateKey key = parsePrivateKey(keyInput); final PrivateKey key = parsePrivateKey(keyInput, password);
if (chainInput != null) { if (chainInput != null) {
chain = CertificateHelper.parseChain(chainInput); chain = CertificateHelper.parseChain(chainInput);
@ -295,7 +303,9 @@ public class CertServiceImpl implements CertService {
if (chainInput != null) { if (chainInput != null) {
validateChain(chain, cert, revocationEnabled); validateChain(chain, cert, revocationEnabled);
} }
} catch (final IOException | CertificateException e) { } catch (final IOException | CertificateException | OperatorCreationException | PKCSException |
NoSuchAlgorithmException | InvalidKeySpecException e) {
logger.warn("Failed to validate certificate", e);
throw new IllegalStateException("Parsing certificate/key failed: " + e.getMessage(), e); throw new IllegalStateException("Parsing certificate/key failed: " + e.getMessage(), e);
} }
} }
@ -370,18 +380,17 @@ public class CertServiceImpl implements CertService {
try { try {
final String data = "ENCRYPT_DATA"; final String data = "ENCRYPT_DATA";
final SecureRandom random = new SecureRandom(); Signature sig = Signature.getInstance("SHA256withRSA");
final Cipher cipher = Cipher.getInstance(pubKey.getAlgorithm()); sig.initSign(privKey);
cipher.init(Cipher.ENCRYPT_MODE, privKey, random); sig.update(data.getBytes());
final byte[] encryptedData = cipher.doFinal(data.getBytes()); byte[] signature = sig.sign();
cipher.init(Cipher.DECRYPT_MODE, pubKey, random); sig.initVerify(pubKey);
final String decreptedData = new String(cipher.doFinal(encryptedData)); sig.update(data.getBytes());
if (!decreptedData.equals(data)) { if (!sig.verify(signature)) {
throw new IllegalStateException("Bad public-private key"); throw new IllegalStateException("Bad public-private key");
} }
} catch (final InvalidKeyException | SignatureException e) {
} catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException e) {
throw new IllegalStateException("Bad public-private key", e); throw new IllegalStateException("Bad public-private key", e);
} catch (final NoSuchAlgorithmException e) { } catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException("Invalid algorithm for public-private key", e); throw new IllegalStateException("Invalid algorithm for public-private key", e);
@ -423,19 +432,55 @@ public class CertServiceImpl implements CertService {
} }
public PrivateKey parsePrivateKey(final String key) throws IOException { public PrivateKey parsePrivateKey(final String key, String password) throws IOException, OperatorCreationException, PKCSException, NoSuchAlgorithmException, InvalidKeySpecException {
Preconditions.checkArgument(StringUtils.isNotEmpty(key)); Preconditions.checkArgument(StringUtils.isNotEmpty(key));
try (final PemReader pemReader = new PemReader(new StringReader(key));) { PEMParser pemParser = new PEMParser(new StringReader(key));
final PemObject pemObject = pemReader.readPemObject(); Object privateKeyObj = pemParser.readObject();
final byte[] content = pemObject.getContent(); if (privateKeyObj == null) {
final PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content); throw new CloudRuntimeException("Cannot parse private key");
final KeyFactory factory = KeyFactory.getInstance("RSA", "BC");
return factory.generatePrivate(privKeySpec);
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new IOException("No encryption provider available.", e);
} catch (final InvalidKeySpecException e) {
throw new IOException("Invalid Key format.", e);
} }
PrivateKey privateKey;
if (privateKeyObj instanceof PKCS8EncryptedPrivateKeyInfo) {
privateKey = parsePKCS8EncryptedPrivateKeyInfo((PKCS8EncryptedPrivateKeyInfo)privateKeyObj, password);
} else if (privateKeyObj instanceof PEMEncryptedKeyPair) {
privateKey = parsePEMEncryptedKeyPair((PEMEncryptedKeyPair)privateKeyObj, password);
} else if (privateKeyObj instanceof PEMKeyPair) {
// Key pair
PEMKeyPair pemKeyPair = (PEMKeyPair) privateKeyObj;
privateKey = new JcaPEMKeyConverter().getKeyPair(pemKeyPair).getPrivate();
} else if (privateKeyObj instanceof PrivateKeyInfo) {
// Private key only
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) privateKeyObj;
privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
} else {
throw new IllegalArgumentException("Unsupported PEM object: " + privateKeyObj.getClass());
}
pemParser.close();
return privateKey;
}
private PrivateKey parsePKCS8EncryptedPrivateKeyInfo(PKCS8EncryptedPrivateKeyInfo privateKeyObj, String password)
throws IOException, OperatorCreationException, PKCSException, NoSuchAlgorithmException, InvalidKeySpecException {
if (password == null) {
throw new CloudRuntimeException("Key is encrypted by PKCS#8 but password is null");
}
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo)privateKeyObj;
JceOpenSSLPKCS8DecryptorProviderBuilder builder = new JceOpenSSLPKCS8DecryptorProviderBuilder();
InputDecryptorProvider decryptor = builder.build(password.toCharArray());
PrivateKeyInfo privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptor);
String algorithm = privateKeyInfo.getPrivateKeyAlgorithm().getAlgorithm().getId();
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
return keyFactory.generatePrivate(keySpec);
}
private PrivateKey parsePEMEncryptedKeyPair(PEMEncryptedKeyPair encryptedKeyPair, String password) throws IOException {
if (password == null) {
throw new CloudRuntimeException("Key is encrypted but password is null");
}
return new JcaPEMKeyConverter().getKeyPair(
encryptedKeyPair.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray()))).getPrivate();
} }
@Override @Override

View File

@ -17,12 +17,30 @@
package com.cloud.network.lb; package com.cloud.network.lb;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network; import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.dao.LoadBalancerCertMapDao;
import com.cloud.network.dao.LoadBalancerCertMapVO;
import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVO; import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.SslCertVO;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.loadbalancer.UpdateLoadBalancerRuleCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -32,11 +50,16 @@ import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.Spy; import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@ -48,10 +71,39 @@ public class LoadBalancingRulesManagerImplTest{
@Mock @Mock
NetworkOrchestrationService _networkMgr; NetworkOrchestrationService _networkMgr;
@Mock
LoadBalancerDao _lbDao;
@Mock
EntityManager _entityMgr;
@Mock
AccountManager _accountMgr;
@Mock
NetworkModel _networkModel;
@Mock
LoadBalancerCertMapDao _lbCertMapDao;
@Mock
NetworkOfferingServiceMapDao _networkOfferingServiceDao;
@Spy @Spy
@InjectMocks @InjectMocks
LoadBalancingRulesManagerImpl lbr = new LoadBalancingRulesManagerImpl(); LoadBalancingRulesManagerImpl lbr = new LoadBalancingRulesManagerImpl();
@Mock
NetworkVO networkMock;
@Mock
LoadBalancerVO loadBalancerMock;
private long accountId = 10L;
private long lbRuleId = 2L;
private long certMapRuleId = 3L;
private long networkId = 4L;
@Test @Test
public void generateCidrStringTestNullCidrList() { public void generateCidrStringTestNullCidrList() {
String result = lbr.generateCidrString(null); String result = lbr.generateCidrString(null);
@ -83,7 +135,7 @@ public class LoadBalancingRulesManagerImplTest{
List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter); List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter);
when(loadBalancerMock.getNetworkId()).thenReturn(10L); when(loadBalancerMock.getNetworkId()).thenReturn(10L);
when(_networkDao.findById(Mockito.anyLong())).thenReturn(networkMock); when(_networkDao.findById(anyLong())).thenReturn(networkMock);
when(_networkMgr.getProvidersForServiceInNetwork(networkMock, Network.Service.Lb)).thenReturn(providers); when(_networkMgr.getProvidersForServiceInNetwork(networkMock, Network.Service.Lb)).thenReturn(providers);
Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock); Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock);
@ -101,4 +153,159 @@ public class LoadBalancingRulesManagerImplTest{
Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock); Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock);
} }
@Test
public void testAssignCertToLoadBalancer() throws Exception {
long accountId = 10L;
long lbRuleId = 2L;
long certId = 3L;
long networkId = 4L;
AccountVO account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid");
account.setId(accountId);
UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone",
UUID.randomUUID().toString(), User.Source.UNKNOWN);
CallContext.register(user, account);
LoadBalancerVO loadBalancerMock = Mockito.mock(LoadBalancerVO.class);
when(_lbDao.findById(lbRuleId)).thenReturn(loadBalancerMock);
when(loadBalancerMock.getId()).thenReturn(lbRuleId);
when(loadBalancerMock.getAccountId()).thenReturn(accountId);
when(loadBalancerMock.getNetworkId()).thenReturn(networkId);
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
SslCertVO certVO = Mockito.mock(SslCertVO.class);
when(_entityMgr.findById(SslCertVO.class, certId)).thenReturn(certVO);
when(certVO.getAccountId()).thenReturn(accountId);
LoadBalancerCertMapVO certMapRule = Mockito.mock(LoadBalancerCertMapVO.class);
when(_lbCertMapDao.findByLbRuleId(lbRuleId)).thenReturn(certMapRule);
Mockito.doNothing().when(_accountMgr).checkAccess(Mockito.any(Account.class), Mockito.isNull(SecurityChecker.AccessType.class), Mockito.eq(true), Mockito.any(LoadBalancerVO.class));
Mockito.doReturn("LB").when(lbr).getLBCapability(networkId, Network.Capability.SslTermination.getName());
Mockito.doReturn(true).when(lbr).applyLoadBalancerConfig(lbRuleId);
lbr.assignCertToLoadBalancer(lbRuleId, certId, true);
Mockito.verify(lbr, times(2)).applyLoadBalancerConfig(lbRuleId);
}
private void setupUpdateLoadBalancerRule() throws Exception{
AccountVO account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid");
account.setId(accountId);
UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone",
UUID.randomUUID().toString(), User.Source.UNKNOWN);
CallContext.register(user, account);
when(_lbDao.findById(lbRuleId)).thenReturn(loadBalancerMock);
when(loadBalancerMock.getId()).thenReturn(lbRuleId);
when(loadBalancerMock.getNetworkId()).thenReturn(networkId);
when(_networkDao.findById(networkId)).thenReturn(networkMock);
Mockito.doNothing().when(_accountMgr).checkAccess(Mockito.any(Account.class), Mockito.isNull(SecurityChecker.AccessType.class), Mockito.eq(true), Mockito.any(LoadBalancerVO.class));
LoadBalancingRule loadBalancingRule = Mockito.mock(LoadBalancingRule.class);
Mockito.doReturn(loadBalancingRule).when(lbr).getLoadBalancerRuleToApply(loadBalancerMock);
Mockito.doReturn(true).when(lbr).validateLbRule(loadBalancingRule);
Mockito.doReturn(true).when(lbr).applyLoadBalancerConfig(lbRuleId);
when(_lbDao.update(lbRuleId, loadBalancerMock)).thenReturn(true);
LoadBalancerCertMapVO certMapRule = Mockito.mock(LoadBalancerCertMapVO.class);
when(_lbCertMapDao.findByLbRuleId(lbRuleId)).thenReturn(certMapRule);
when(certMapRule.getId()).thenReturn(certMapRuleId);
}
@Test
public void testUpdateLoadBalancerRule1() throws Exception {
setupUpdateLoadBalancerRule();
// Update protocol from TCP to SSL
UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.TCP_PROTO).thenReturn(NetUtils.SSL_PROTO);
lbr.updateLoadBalancerRule(cmd);
Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
}
@Test
public void testUpdateLoadBalancerRule2() throws Exception {
setupUpdateLoadBalancerRule();
// Update protocol from SSL to TCP
UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.TCP_PROTO);
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO).thenReturn(NetUtils.TCP_PROTO);
lbr.updateLoadBalancerRule(cmd);
Mockito.verify(_lbCertMapDao, times(1)).remove(anyLong());
Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
}
@Test
public void testUpdateLoadBalancerRule3() throws Exception {
setupUpdateLoadBalancerRule();
// Update algorithm from source to roundrobin, lb protocol is same
UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
ReflectionTestUtils.setField(cmd, "algorithm", "roundrobin");
ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
when(loadBalancerMock.getAlgorithm()).thenReturn("source");
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
lbr.updateLoadBalancerRule(cmd);
Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
}
@Test
public void testUpdateLoadBalancerRule4() throws Exception {
setupUpdateLoadBalancerRule();
// Update with same algorithm and protocol
UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
ReflectionTestUtils.setField(cmd, "algorithm", "roundrobin");
ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
when(loadBalancerMock.getAlgorithm()).thenReturn("roundrobin");
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
lbr.updateLoadBalancerRule(cmd);
Mockito.verify(lbr, never()).applyLoadBalancerConfig(lbRuleId);
Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
}
@Test(expected = CloudRuntimeException.class)
public void testUpdateLoadBalancerRule5() throws Exception {
setupUpdateLoadBalancerRule();
// Update protocol from SSL to TCP, throws an exception
UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.TCP_PROTO);
when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO).thenReturn(NetUtils.TCP_PROTO);
Mockito.doThrow(ResourceUnavailableException.class).when(lbr).applyLoadBalancerConfig(lbRuleId);
List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter);
when(_networkDao.findById(anyLong())).thenReturn(networkMock);
when(_networkMgr.getProvidersForServiceInNetwork(networkMock, Network.Service.Lb)).thenReturn(providers);
lbr.updateLoadBalancerRule(cmd);
Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.TCP_PROTO);
Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.SSL_PROTO);
}
} }

View File

@ -40,6 +40,7 @@ import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao; import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.LoadBalancerDao; import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVMMapDao; import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.MonitoringServiceDao; import com.cloud.network.dao.MonitoringServiceDao;
import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.NetworkVO;
@ -54,6 +55,8 @@ import com.cloud.network.dao.Site2SiteVpnGatewayDao;
import com.cloud.network.dao.UserIpv6AddressDao; import com.cloud.network.dao.UserIpv6AddressDao;
import com.cloud.network.dao.VirtualRouterProviderDao; import com.cloud.network.dao.VirtualRouterProviderDao;
import com.cloud.network.dao.VpnUserDao; import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.lb.LoadBalancingRule;
import com.cloud.network.lb.LoadBalancingRulesManager;
import com.cloud.network.rules.dao.PortForwardingRulesDao; import com.cloud.network.rules.dao.PortForwardingRulesDao;
import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao; import com.cloud.network.vpc.dao.VpcDao;
@ -67,6 +70,7 @@ import com.cloud.storage.dao.VolumeDao;
import com.cloud.user.dao.UserDao; import com.cloud.user.dao.UserDao;
import com.cloud.user.dao.UserStatisticsDao; import com.cloud.user.dao.UserStatisticsDao;
import com.cloud.user.dao.UserStatsLogDao; import com.cloud.user.dao.UserStatsLogDao;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DomainRouterVO; import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineManager;
@ -259,6 +263,9 @@ public class VirtualNetworkApplianceManagerImplTest {
@Mock @Mock
private BGPService bgpService; private BGPService bgpService;
@Mock
private LoadBalancingRulesManager _lbMgr;
// @InjectMocks // @InjectMocks
// private VirtualNetworkApplianceManagerImpl virtualNetworkApplianceManagerImpl; // private VirtualNetworkApplianceManagerImpl virtualNetworkApplianceManagerImpl;
@ -391,4 +398,21 @@ public class VirtualNetworkApplianceManagerImplTest {
Mockito.verify(_commandSetupHelper).createBgpPeersCommands(bgpPeers, router, cmds, network); Mockito.verify(_commandSetupHelper).createBgpPeersCommands(bgpPeers, router, cmds, network);
} }
@Test
public void testUpdateWithLbRuleSslCertificates() {
StringBuilder loadBalancingData = new StringBuilder();
LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class);
when(loadBalancer.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
when(loadBalancer.getId()).thenReturn(1L);
when(loadBalancer.getSourcePortStart()).thenReturn(443);
LoadBalancingRule.LbSslCert lbSslCert = Mockito.mock(LoadBalancingRule.LbSslCert.class);
when(lbSslCert.isRevoked()).thenReturn(false);
when(_lbMgr.getLbSslCert(1L)).thenReturn(lbSslCert);
String sourceIp = "1.2.3.4";
virtualNetworkApplianceManagerImpl.updateWithLbRuleSslCertificates(loadBalancingData, loadBalancer, sourceIp);
Assert.assertEquals(",sslcert=1_2_3_4-443.pem", loadBalancingData.toString());
}
} }

View File

@ -201,6 +201,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
Mockito.when(_sshKeyPairDao.listKeyPairs(Mockito.anyLong(), Mockito.anyLong())).thenReturn(sshkeyList); Mockito.when(_sshKeyPairDao.listKeyPairs(Mockito.anyLong(), Mockito.anyLong())).thenReturn(sshkeyList);
Mockito.when(_sshKeyPairDao.remove(Mockito.anyLong())).thenReturn(true); Mockito.when(_sshKeyPairDao.remove(Mockito.anyLong())).thenReturn(true);
Mockito.when(userDataDao.removeByAccountId(Mockito.anyLong())).thenReturn(222); Mockito.when(userDataDao.removeByAccountId(Mockito.anyLong())).thenReturn(222);
Mockito.when(sslCertDao.removeByAccountId(Mockito.anyLong())).thenReturn(333);
Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong()); Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong());
Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any()); Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any());

View File

@ -29,6 +29,7 @@ import com.cloud.network.dao.AccountGuestVlanMapDao;
import com.cloud.network.dao.IPAddressDao; import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.RemoteAccessVpnDao; import com.cloud.network.dao.RemoteAccessVpnDao;
import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao; import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.security.SecurityGroupManager; import com.cloud.network.security.SecurityGroupManager;
import com.cloud.network.security.dao.SecurityGroupDao; import com.cloud.network.security.dao.SecurityGroupDao;
@ -198,6 +199,8 @@ public class AccountManagetImplTestBase {
@Mock @Mock
UserDataDao userDataDao; UserDataDao userDataDao;
@Mock @Mock
SslCertDao sslCertDao;
@Mock
NetworkPermissionDao networkPermissionDaoMock; NetworkPermissionDao networkPermissionDaoMock;
@Spy @Spy

View File

@ -34,6 +34,13 @@ import com.cloud.utils.db.TransactionLegacy;
import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd; import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd;
import org.apache.cloudstack.api.command.user.loadbalancer.UploadSslCertCmd; import org.apache.cloudstack.api.command.user.loadbalancer.UploadSslCertCmd;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.bouncycastle.openssl.PKCS8Generator;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OutputEncryptor;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
@ -44,9 +51,13 @@ import org.mockito.Mockito;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -207,7 +218,7 @@ public class CertServiceTest {
} }
} }
// @Test @Test
/** /**
* Given a Self-signed Certificate with encrypted key, upload should succeed * Given a Self-signed Certificate with encrypted key, upload should succeed
*/ */
@ -456,7 +467,7 @@ public class CertServiceTest {
Assert.fail("Given an encrypted private key with a bad password. Upload should fail."); Assert.fail("Given an encrypted private key with a bad password. Upload should fail.");
} catch (final Exception e) { } catch (final Exception e) {
Assert.assertTrue("Did not expect message: " + e.getMessage(), Assert.assertTrue("Did not expect message: " + e.getMessage(),
e.getMessage().contains("Parsing certificate/key failed: Invalid Key format.")); e.getMessage().contains("Parsing certificate/key failed: exception using cipher - please check password and data."));
} }
} }
@ -544,7 +555,7 @@ public class CertServiceTest {
Assert.fail("Given a private key which has a different algorithm than the certificate, upload should fail"); Assert.fail("Given a private key which has a different algorithm than the certificate, upload should fail");
} catch (final Exception e) { } catch (final Exception e) {
Assert.assertTrue("Did not expect message: " + e.getMessage(), Assert.assertTrue("Did not expect message: " + e.getMessage(),
e.getMessage().contains("Parsing certificate/key failed: Invalid Key format.")); e.getMessage().contains("Public and private key have different algorithms"));
} }
} }
@ -821,4 +832,283 @@ public class CertServiceTest {
return 1; return 1;
} }
} }
private String generateEncryptedPrivateKey(String password) throws NoSuchAlgorithmException, OperatorCreationException, IOException {
// Generate RSA key pair
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair keyPair = kpg.generateKeyPair();
// Build encryptor (AES-256-CBC is FIPS-approved)
OutputEncryptor encryptor = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC)
.setPassword(password.toCharArray())
.build();
// Wrap the private key into PKCS#8 format and encrypt
JcaPKCS8Generator gen = new JcaPKCS8Generator(keyPair.getPrivate(), encryptor);
PemObject pemObject = gen.generate();
StringWriter stringWriter = new StringWriter();
try (PemWriter pemWriter = new PemWriter(stringWriter)) {
pemWriter.writeObject(pemObject);
}
return stringWriter.toString();
}
@Test
public void parseEncryptedPrivateKey() throws Exception{
String password = "strongpassword";
String key = generateEncryptedPrivateKey(password);
final CertServiceImpl certService = new CertServiceImpl();
certService.parsePrivateKey(key, password);
}
@Test
public void validateCertAndChainsWithEncryptedKey() {
String password = "strongpassword";
String key = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
"MIIFGzBVBgkqhkiG9w0BBQ0wSDAnBgkqhkiG9w0BBQwwGgQUiQiFcfHTx8EKYNHJ\n" +
"zOqT8/9AkaQCAggAMB0GCWCGSAFlAwQBKgQQKXBglXgHYSWK20BxSFUVLQSCBMBr\n" +
"ro2dXjsEoZfglccP5YWRPETSXntMdjAd39ftiWSXwQWZmht9/t+hSK+qZnGX/8VI\n" +
"0OR7x+8SBDqZAb9mYZzPPcUd/k+KLpQAFBSFrWVle40MY1OyZqEdQe3ELDERS919\n" +
"WRGmjTYUomL1zCAIrx27Woq5iiZkqsXmCcQwKRkCSNbTXjDe6gXtO9ePuMgvSiGg\n" +
"q2rhBZv82AYoc/IHzftsoS53Sda96RE93MK12+L48E5gxbqeHUJeGhn1hxxkqFcj\n" +
"cL/z817M6a9BEJkNlS4sZk3+Fg1RYBTx7CKYzR8WAf+LvasdO5ijPrNcqc6DzIIn\n" +
"tL0Kj/Gjp6rFP83IfezCtVdYi/dRLR9dNROJt7aIaeXnYdYF8o+vmWZm5H4bZeun\n" +
"czadKzd4EfvatHXi7Zq/cV/mh/NitUfnYMR5LUnX9pjNRkr2uqYx5AiO6aPQoR9G\n" +
"Gv1ubkUtug/rDoywwol7XGWxnDNbB4fvXRIGsyZYDh9J1CX+sv693ZeRx1J48vhT\n" +
"s+gZug8oG5DfSLCVaJDuIyHQGKuRLnh6LawUkyCA7Q/9vgmXnXo+0hJ5dYQw21fj\n" +
"M5yHrOt/tway5tJgDwuD778r3Y4w1H9Yt42J3tZL3gOIOyYhHad2M/emh5Khh/m/\n" +
"VK8eM86OQeo/zp+RddM4ckaUxKe/bFBqj9KvhzHsFTAuirT7be3+Ye1iBqKLvCgO\n" +
"yOTY14J1NbrvGmUs6yq3JxTkzl4+A23SPlHQE16j3UzCz0qnNTYLruUibL940uXu\n" +
"rnBcOuW6uM4yc+X3Aqo7xL3kzW/9waCd/VG/btJLNPKSDDRuuKQ7NEPjS+xajmqh\n" +
"WVMzzcMj4wVvTz6vnZNm9u9Yu/ACpzHTD+hVeFZhIscVCdT+LncEumLHHhrLQS3h\n" +
"9gVlv0MvSrWH6sl3oQEnA5ceEI4LfH6eT++IGAdKJTqkpAwSEtSEV+P/dETRNnsH\n" +
"TsKNEdylH++9Ljhkt68971cLGHf9yuzVU75BPFybngcNFZu3+YUDWY0fBwqwE0OI\n" +
"FXeqPhnN2UfAoeqCwz2KtPf2ig0a34S6Rxne9/XewlCsKEGSrdYG8mm4eJzsP69/\n" +
"5qw1MDO1nvt0B5jSly3vHcHGvgiDtG+vsfGqC1TA8eaTSq/UkUAKfoGg1DkL8olz\n" +
"b7jB24748Oh87Ksz12yeyY5T1edpoDcScCRLwIb0vNMKqIUe1aCEdTl08UHV3CbG\n" +
"7rnRLWE+9/Csij2fpkx0mEDeXdLxeSvkw5K8ha26s52MR4WhW0EUN74FJOMrTej3\n" +
"0jtcTC/bThc5jmQDaSQJbaiSIEKl8sdA0u8oTzBD2B1F9gkrZNZpE7hz670tysQs\n" +
"2Z0AxDcxQ7Qfkytg52MfJvLf0jxuNqjfbmQqkQsT+yUkjT6AmOgUMGP4zojP8ErY\n" +
"AvAqgurefHMS/HA8BUT7qxt300cTYaAONUlAJ/qAJ/YoHOI5yqWzBFJsr95NC13t\n" +
"rGqiOOLGtSIxk4WwdUX0u9TW8Hk6pWnl6MkyAn+a3RqKfrJ2tfKMjsO3iqu3Dlvz\n" +
"72RD5LsGcnhfKQ/TdswEA1EKdHBBjnDQOGdWNNTXnn41XoNNKneFjlFgJc8AXyoN\n" +
"fHvkc2aKb86WdpcANxK3\n" +
"-----END ENCRYPTED PRIVATE KEY-----";
String certificate = "-----BEGIN CERTIFICATE-----\n" +
"MIIERTCCAi0CFF2Ro8QjYcOCALfEqL1zs2T0cQzyMA0GCSqGSIb3DQEBCwUAMF4x\n" +
"CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
"CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMCAX\n" +
"DTI1MDYxNzA3MzQxOFoYDzIxMjUwNTI0MDczNDE4WjBeMQswCQYDVQQGEwJYWDEL\n" +
"MAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMwEQYDVQQKDApDbG91ZFN0YWNrMQ8w\n" +
"DQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFwYWNoZTCCASIwDQYJKoZIhvcNAQEB\n" +
"BQADggEPADCCAQoCggEBAMXpfAyO1m+YolspmNL64cMJ0mW4QiJUrrNxYyIaakfW\n" +
"/qs78hMlf8V82T94ayoMs2fpkjf69QsXTZoOZoUkaz58Wz9Z860OMAD/wguGz7EX\n" +
"Bk+OTEDhXP9NAkY99TqscWS3bm6XSu3w0cOwjwLtV72VsT2UA1d0hpVI4kVTbI56\n" +
"RZ1ymboyu/mhp2dqZu+Ewh8n7PMYvDO6hGuqsM5We2WLdSCmPZKtmbQ8CRj0fwJI\n" +
"CZZEafFEBwLhW3F15SRZLxQApzqMTlmbk9edEgOfJZqMrr+F8jguce7Qry6FcbkU\n" +
"6x4oRyykuz5pi5mPjaTxQyY4NWsCHojlQ0kz0VeBUX0CAwEAATANBgkqhkiG9w0B\n" +
"AQsFAAOCAgEAJAUldK70IoyA0jokXfAyLXNRX43/UfmQMu3xvVYI9OPk8f6CrBIm\n" +
"g79cA3pGPNxyIReqFxDk+wXW+/iPCgOwv+YYODPEMZi1Cc8WQJ4OGzovD5hep7TA\n" +
"pg6jo16LdKpOQM6C9XUce3vZf6t487PCgg8SzldqhMMC97Kw+DAxYg+JRd28jfIB\n" +
"RAtpOCzqKqWp7lQ1YwS9M/VI0mYtmiuQbaz1to4qBPcCbR1GsLsmqMmTUkbYYyFF\n" +
"fgvInITyW+0NV/UwgiNFxU+k9T2H1lfvqj6hVRwwj7i84xAu4Y/N9zP/UKXxU93N\n" +
"ogoHabfGcsFEygyTkFuI4XG/Ppc3c8CJV2NbVQixe5Wdt1Yc9qMkbq+OdGvsOhbt\n" +
"T2+Qz5JZ7w0LsYONzuCRbaDpJiAg2MiALe3L1RzEya57/PylgUeH6gMbPyuQ2EyL\n" +
"pTUQ1imV3tTlkxjy7niu/IeqgcQOA2cx8Fwok+ECLvxc47noUlgPcROz5i43+IYA\n" +
"frvGqDfZCeKXKuAi//8wBl2tptMMmLpkS4mW/8Pijcx3JuxC6ySeOFAVgPjq4krw\n" +
"dGl+IBNwKNcsUu5/3uj/2h85w56Ys8uxeLkLqEq+9yHlwxexGJG0qJ2QcXFnOxCC\n" +
"qz+L2k3m0+Yu5zUFsMCTgEwQeR6CUfW9/GtPunZtvwHOSbVus0DvnSE=\n" +
"-----END CERTIFICATE-----";
String certChains = "-----BEGIN CERTIFICATE-----\n" +
"MIIFQzCCAysCFEVQffqr0ScjpyZ6pmDsOOu71t70MA0GCSqGSIb3DQEBCwUAMF4x\n" +
"CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
"CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMB4X\n" +
"DTI1MDYxNjEwMjc1NloXDTMwMDYxNTEwMjc1NlowXjELMAkGA1UEBhMCWFgxCzAJ\n" +
"BgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEGA1UECgwKQ2xvdWRTdGFjazEPMA0G\n" +
"A1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFjaGUwggIiMA0GCSqGSIb3DQEBAQUA\n" +
"A4ICDwAwggIKAoICAQCLiQmSjrht15R1F+r79m/LZN5hsfQBGp+dy+yrtsWfOOur\n" +
"RdXAwgbLxxsyKMQKWCQxlRI7wdhqh0L0ZBrIr9MjltYqsqLAoLmgY4eG/f6G8YGr\n" +
"O/rxzfwTLbCeaIseF/OMA6Sz125HXYp1bltYK4LsuC7tihZXbeVa5pUGs3Jwgcfx\n" +
"LYm4eB42Hp7Eg05uL8LbwT/1AjcwoWkTewKAWXA83zgLRDFDbl1t0IPHI4cdVvia\n" +
"BNwNbG49ZCF6OgmokSarQSe4Vbems1u9T9pAySXAVjEYBqFjKWyswpdr782uNLmB\n" +
"lCGm0pDeJ9/WASxbTJr7k9H6ZpnaHr54DG6ZqennWMz8w6r2pf7bp/EGZ3mZQ4s3\n" +
"5ylSP4cQt8CSSI8k2CflPGUyytUAiWlDS3qSyIuAOPKXDg7wIpcbwcu4VMeKnH0Z\n" +
"x7Uu9j1UDZEZoSu6UI/VInTl47k1/ECD+AO9yBzZSv+pTQmO3/Im3CcxsTHmVd5s\n" +
"Tl0CJ/jWNpo9DAMtmGvt6CBWBXGRsO2XNk7djRcq2CubiCpvODg+7CcR6CiZK73L\n" +
"1aOisLiq3+ofiJSSXRRuKtJlkQ4eSPSbYWkNJcKmIhbCoYOdH/Pe3/+RHjvNc1kO\n" +
"OUb+icmfzcMVAs3C5jybpazsfjDNQZXWAFx4FLDcqOVbrCwom+tMukw+hzlZnwID\n" +
"AQABMA0GCSqGSIb3DQEBCwUAA4ICAQAdexoMwn+Ol1A3+NOHk9WJZX+t2Q8/9wWb\n" +
"K+jSVleSfXXWsB1mC3fABVJQdCxtXCH3rpt4w7FK6aUes9VjqAHap4tt9WBq0Wqq\n" +
"vvMURFHfllxEM31Z35kBOCSQY9xwpGV1rRh/zYs4h55YixomR3nXNZ9cI8xzlSCi\n" +
"sMG0mv0y+yxPohKrZj3AzLYz/M11SimSoyRPIANI+cUg1nJXyQoHzVHWEp1Nj0HB\n" +
"M/GW05cxsWea7f5YcAW1JQI3FOkpwb72fIZOtMDa4PO8IYWXJAeAc/chw745/MTi\n" +
"Rvl2NT4RZBAcrSNbhCOzRPG/ZiG+ArQuCluZ9HHAXRBMTtlLk5DO4+XxZlyGpjwf\n" +
"uKniK8dccy9uU0ho73p9SNDhXH0yb9Naj8vd9NWzCUYaaBXt/92cIyhaAHAVFxJu\n" +
"o6jr2FLbnhSGF9EO/tHvF7LxZv1dnbInvlWHwoFQjwmoeB+e17lHBdPMnWnPKBZe\n" +
"jA2VH/IzGCucWuWQhruummO5GT8Z6F4jBwvafBo+QARKPZgEBpx3LycXrpkYI3LT\n" +
"GGOpGCxFt5tVZOEsC/jQ5rIljNSeTzWmzfNRn/yRUW97uWsrzcQIBAUtu/pQnyFQ\n" +
"WCnC1ipCp1zhJsXAFUKuqEfLngXodOvC4tAOr76h11S57o5lN4506Poq2mWgAZe/\n" +
"JZr9MEn1+w==\n" +
"-----END CERTIFICATE-----\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIFnzCCA4egAwIBAgIUcUNMqgWoDLsvMj0YmEudj60EG5swDQYJKoZIhvcNAQEL\n" +
"BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG\n" +
"A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj\n" +
"aGUwIBcNMjUwNjE2MTAyNzM2WhgPMjEyNTA1MjMxMDI3MzZaMF4xCzAJBgNVBAYT\n" +
"AlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoMCkNsb3VkU3Rh\n" +
"Y2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMIICIjANBgkqhkiG\n" +
"9w0BAQEFAAOCAg8AMIICCgKCAgEAwVQaePulUM523gKw168ToYp+gt05bXbu4Gg8\n" +
"uaRDKhnRAX1sEgYwkQ36Q+iTDEM9sKRma8lMNMIqkZMQdk6sIGX6BL+6wUOb7mL0\n" +
"5+I0yO9i8ooaGgNaeNvZftNIRlLsnPMGJaeom2/66XV4CsMqoZKaJ1H/I8N+bAeD\n" +
"GvrBx+B4l9D3G390nQvot9JUzrJgGuLl0KDHapvhlR39cCgEfIii02uX1iy0qXlV\n" +
"b+G1kLvpeC7T+lsJxondPJ69aO3lbDv/izyWw7qqBC57UhT/oKDxJmjQqklqzhgt\n" +
"nM/p3YE7M0nkRi3LnRmsZBz7o1DRf+M29zypKzXVk1aJflL46AtLMmpDIzVrEB2M\n" +
"q7o47rstXusYRYsBCqGTgdI1fV/CkDsZY5XkPZh2dsjZCHIS4P03OqFGsc6PQha2\n" +
"+y2AhV1pvywkDl48kPKSukHfV1RtaPZUZtcQKztwHH+aFfo9mD8z0H2HcExdXKzd\n" +
"jhRhI9ZSwFj3HEN9f5P8fS3lf5+fV7EEbG4NisieBj/UivW6QiTHpLD7wRLIUt2g\n" +
"XgXNF0lfJzYHbIcxQ6kfC5McU2fu6mUC+p/pNN8G0POS3S2T55tEUqLL4N0SadQy\n" +
"N1TZlTd2xTn+Hb6WlG0f5m97xGcNlGHKBvntFrHvOIfkEQ9ne3MlOO1Gjlintowo\n" +
"fRGf15kCAwEAAaNTMFEwHQYDVR0OBBYEFM4WEQJpN9M07Q8CHq+5owG93Dj8MB8G\n" +
"A1UdIwQYMBaAFM4WEQJpN9M07Q8CHq+5owG93Dj8MA8GA1UdEwEB/wQFMAMBAf8w\n" +
"DQYJKoZIhvcNAQELBQADggIBABr5RKGc/rKx4fOgyXNJR4aCJCTtPZKs4AUCCBYz\n" +
"cWOjJYGNvThOPSVx51nAD8+YP2BwkOIPfhl2u/+vQSSbz4OlauXLki2DUN8E2OFe\n" +
"gJfxPDzWOfAo/QOJcyHwSlnIQjiZzG2lK3eyf5IFnfQILQzDvXaUYvMBkl2hb5Q7\n" +
"44H6tRw78uuf/KsT4rY0bBFMN5DayjiyvoIDUvzCRqcb2KOi9DnZ7pXjduL7tO0j\n" +
"PhlQ24B77LVUUAvydIGUzmbhGC2VvY1qE7uaYgYtgSUZ0zSjJrHjUjVLMzRouNP7\n" +
"jpbBQRAcP4FDcOFZBHogunA0hxQdm0d8u3LqDYPNS0rpfW0ddU/72nfBX4bnoDEN\n" +
"+anw4wOgFuUcoEThALWZ9ESVKxXQ9Fpvd6FRW8fLLqhXAuli1BqP1c1WRxagldYe\n" +
"nPGm/FGZyJ2xOak9Uigi9NAQ/vX6CEfgcJgFZmCo8EKH0d4Ut72vGUcPqiUhT2EI\n" +
"AFAd6drSyoUdXXniSMWky9Vrt+qtLuAD1nhHTv8ZPdItXokoiD6ea/4xrbUZn0qY\n" +
"lLMDyfY76UVF0ruTR2Q6IdSq/zSggdwgkTooOW4XZcRf5l/ZnoeVQ1QH9C85SIKH\n" +
"IKZwPeGUm+EntmpuCBDmQSHLRCGEThd64iOAjqLR6arLj4TBJzBrZsGHFJbm0OcI\n" +
"dwa9\n" +
"-----END CERTIFICATE-----";
final CertServiceImpl certService = new CertServiceImpl();
certService.validate(certificate, key, password, certChains, false);
}
@Test
public void validateCertAndChainsWithUnencryptedKey() {
String key = "-----BEGIN PRIVATE KEY-----\n" +
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCph7jsoMCQirRn\n" +
"3obuvgnnefTXRQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0\n" +
"rXjjgsVT3r4bv+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRx\n" +
"kB0klwUcj/jk/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0q\n" +
"t6KDMkUwv8fyzrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaM\n" +
"Pe7eqSFzxunF9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXq\n" +
"HNUwYkALAgMBAAECggEAK5JiiQ7X7053B6s96uaVDRVfRGTNKa5iMXBNDHq3wbHZ\n" +
"X4IJAVr+PE7ivxdKco3r45fT11X9ZpUsssdTJsZZiTDak69BTiFcaaRCnmqOIlpd\n" +
"J7vb6TMrTIW8RvxQ0M/txm6DuNHLibqJX5a2pszZ13l5cwECfF9/v/XLJTTukCbu\n" +
"6D/f3fBVFl1tM8y9saOEYLkdb4dILWY61bVSDNswgprz2EV1SFnk5jxz2FuBrM/Q\n" +
"+7hINvjDcaRvcm59hRb1rkljv7S10VoNw/CFkU451csJkUe4vWZwB8lZK/XxLQG0\n" +
"HEdS1zU1XY8H8Y1RCrxjGRyiiWsBtUThhWYlPrGCoQKBgQDkP09YAlKqXhT69Kx5\n" +
"keg2i1jV2hA73zWbWXt9xp5jG5r3pl3m170DvKL93YIDnHtpTC56mlzGrzS7DSTN\n" +
"p0buY9Qb3fkJxunCpPVFo0HMFkpeR77ax0v34NzSohlRLKFo5R2M1cmDfbVbnSSl\n" +
"MB57FfRRMxzjrk+dJvjOeJsxjwKBgQC+JLb4B8CZjpurXYg3ySiRqFsCqkqob+kf\n" +
"9dR+rWvcR6vMTEyha0hUlDvTikDepU2smYR4oPHfdcXF9lAJ7T02UmQDeizAqR68\n" +
"u9e+yS0q3tdRnPPZmXJfaDCXG1hKMqF4YA5Vs0XAjleF3zHB+vBLrnlPpShtd/Mu\n" +
"sWTpxICTxQKBgQDSr/n+pE5IQwYczOO0aFGwn5pF9L9NdPHXz5aleETV+TJn7WL6\n" +
"ZiRsoaDWs7SCvtxQS2kP9RM0t5/2FeDmEMXx4aZ2fsSWGM3IxVo+iL+Aswa81n8/\n" +
"Ff5y9lb/+29hNdBcsjk/ukwEG3Lf+UNNVAie15oppgPByzJkPwgmFsAy0wKBgHDX\n" +
"/TZp82WuerhSw/rHiSoYjhqg0bnw4Ju1Gy0q4q5SYqTWS0wpDT4U0wSSMjlwRQ6/\n" +
"9RxZ9/G0RXFc4tdhUkig0PY3VcPpGnLL0BhL8GBW69ZlnVpwdK4meV/UPKucLLPx\n" +
"3dACmszSLSMn+LG0qVNg8mHQFJQS8eGuKcOKePw5AoGACuxtefROKdKOALh4lTi2\n" +
"VOwPZ+1jxsm6lKNccIEvbUpe3UXPgNWpJiDX8mUcob4/NBLzmV3BUVKbG7Exbo5J\n" +
"LoMfp7OsztWUFwt7YAvRfS8fHdhkEsxEf3T72ADieH5ZAuXFF+K0H3r6HtWPD4ws\n" +
"mTJjGP4+Bl/dFakA5FJcjHg=\n" +
"-----END PRIVATE KEY-----";
String certificate = "-----BEGIN CERTIFICATE-----\n" +
"MIIERTCCAi0CFF2Ro8QjYcOCALfEqL1zs2T0cQzzMA0GCSqGSIb3DQEBCwUAMF4x\n" +
"CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
"CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMCAX\n" +
"DTI1MDYxNzA5MTE0N1oYDzIxMjUwNTI0MDkxMTQ3WjBeMQswCQYDVQQGEwJYWDEL\n" +
"MAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMwEQYDVQQKDApDbG91ZFN0YWNrMQ8w\n" +
"DQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFwYWNoZTCCASIwDQYJKoZIhvcNAQEB\n" +
"BQADggEPADCCAQoCggEBAKmHuOygwJCKtGfehu6+Ced59NdFBh320X2TZoJWROKK\n" +
"Ky8Lf2nHswZ2C8NcAOEP1+ZHujJdVCV827SteOOCxVPevhu/4NWLzVgZee5TuHXT\n" +
"II5km3bWLpAYc3UJLT9MB+DRSGUAIIRg1HGQHSSXBRyP+OT8AqKXUOhQam8CbaBm\n" +
"x6T+eNvvr8B4h/Fz4Szhk48JWpA4enqAjSq3ooMyRTC/x/LOulJzKAxQ9Visuabo\n" +
"PSmfzJ3eKFbTvAbEyM0PpSA6dqjy3ofctow97t6pIXPG6cX00UWOdSspwdllKV50\n" +
"Mz/fspupv/xPl/7hLCOKhLU508KHwCl2Jeoc1TBiQAsCAwEAATANBgkqhkiG9w0B\n" +
"AQsFAAOCAgEAOKaT7cp1P/B67cT0pQ+ZO7dazoomvwbznpUDPlX+h2f9pPYvBoOJ\n" +
"qul0Np3zft3sR4M1uxRNuayhd+oFMNx0J3CJVxc6fpUvc0IvNAgy0C6IeAlTTH6V\n" +
"Tiy8X5YeD1SAg0wJkqZQzXC+8Ao+LPacdhnz7wUSV1j4ILlVZcfvISaaZUFidERT\n" +
"nP18syUWSodTULXTKB8M8z/9t6KFWXJDJGXLKBMoX3DCSx9QG5GDMuyu9XWf3bBH\n" +
"ZHZse02mh0x83hV34Bpa1Yr98PsGvQm7GUXiLenFO57wzWaInxBkS6sF4OWreiMI\n" +
"lN94CtBXtMxtC5C50WthNGBJHg3dXKeF3O6F8z8EkkqpKyJtJ3IoAXTHGEh5fxp0\n" +
"tsbOEqJ540XbtD82UWYA4bVY1h0Tb1SaV7fylZkuYXZ+rl6G0S7roPVYbrjRsP9t\n" +
"FCGko35WkhkI0OpNoTremH+H1U/nBowMm6tSfZ0ZWa/4NnLacXhPjDJkEhu7RlA4\n" +
"JYeYKe4dj4hLdcHCUFuP8Tdv1P20SGQQOaHUXYbHP5Er3EHZxzI13JwHiO+FKuYP\n" +
"igIqbCdBd8smTzdbit0f6OfKOyNXDDxN+E1VKAHSquYuxMcj+njKTQ1ihpXnTLpo\n" +
"ZP3NoLZ6gAQIjEgHHsLeZ24HCbiFfUpwWSPNNcr6X5qQelt5leNGsIU=\n" +
"-----END CERTIFICATE-----";
String certChains = "-----BEGIN CERTIFICATE-----\n" +
"MIIFQzCCAysCFEVQffqr0ScjpyZ6pmDsOOu71t70MA0GCSqGSIb3DQEBCwUAMF4x\n" +
"CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
"CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMB4X\n" +
"DTI1MDYxNjEwMjc1NloXDTMwMDYxNTEwMjc1NlowXjELMAkGA1UEBhMCWFgxCzAJ\n" +
"BgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEGA1UECgwKQ2xvdWRTdGFjazEPMA0G\n" +
"A1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFjaGUwggIiMA0GCSqGSIb3DQEBAQUA\n" +
"A4ICDwAwggIKAoICAQCLiQmSjrht15R1F+r79m/LZN5hsfQBGp+dy+yrtsWfOOur\n" +
"RdXAwgbLxxsyKMQKWCQxlRI7wdhqh0L0ZBrIr9MjltYqsqLAoLmgY4eG/f6G8YGr\n" +
"O/rxzfwTLbCeaIseF/OMA6Sz125HXYp1bltYK4LsuC7tihZXbeVa5pUGs3Jwgcfx\n" +
"LYm4eB42Hp7Eg05uL8LbwT/1AjcwoWkTewKAWXA83zgLRDFDbl1t0IPHI4cdVvia\n" +
"BNwNbG49ZCF6OgmokSarQSe4Vbems1u9T9pAySXAVjEYBqFjKWyswpdr782uNLmB\n" +
"lCGm0pDeJ9/WASxbTJr7k9H6ZpnaHr54DG6ZqennWMz8w6r2pf7bp/EGZ3mZQ4s3\n" +
"5ylSP4cQt8CSSI8k2CflPGUyytUAiWlDS3qSyIuAOPKXDg7wIpcbwcu4VMeKnH0Z\n" +
"x7Uu9j1UDZEZoSu6UI/VInTl47k1/ECD+AO9yBzZSv+pTQmO3/Im3CcxsTHmVd5s\n" +
"Tl0CJ/jWNpo9DAMtmGvt6CBWBXGRsO2XNk7djRcq2CubiCpvODg+7CcR6CiZK73L\n" +
"1aOisLiq3+ofiJSSXRRuKtJlkQ4eSPSbYWkNJcKmIhbCoYOdH/Pe3/+RHjvNc1kO\n" +
"OUb+icmfzcMVAs3C5jybpazsfjDNQZXWAFx4FLDcqOVbrCwom+tMukw+hzlZnwID\n" +
"AQABMA0GCSqGSIb3DQEBCwUAA4ICAQAdexoMwn+Ol1A3+NOHk9WJZX+t2Q8/9wWb\n" +
"K+jSVleSfXXWsB1mC3fABVJQdCxtXCH3rpt4w7FK6aUes9VjqAHap4tt9WBq0Wqq\n" +
"vvMURFHfllxEM31Z35kBOCSQY9xwpGV1rRh/zYs4h55YixomR3nXNZ9cI8xzlSCi\n" +
"sMG0mv0y+yxPohKrZj3AzLYz/M11SimSoyRPIANI+cUg1nJXyQoHzVHWEp1Nj0HB\n" +
"M/GW05cxsWea7f5YcAW1JQI3FOkpwb72fIZOtMDa4PO8IYWXJAeAc/chw745/MTi\n" +
"Rvl2NT4RZBAcrSNbhCOzRPG/ZiG+ArQuCluZ9HHAXRBMTtlLk5DO4+XxZlyGpjwf\n" +
"uKniK8dccy9uU0ho73p9SNDhXH0yb9Naj8vd9NWzCUYaaBXt/92cIyhaAHAVFxJu\n" +
"o6jr2FLbnhSGF9EO/tHvF7LxZv1dnbInvlWHwoFQjwmoeB+e17lHBdPMnWnPKBZe\n" +
"jA2VH/IzGCucWuWQhruummO5GT8Z6F4jBwvafBo+QARKPZgEBpx3LycXrpkYI3LT\n" +
"GGOpGCxFt5tVZOEsC/jQ5rIljNSeTzWmzfNRn/yRUW97uWsrzcQIBAUtu/pQnyFQ\n" +
"WCnC1ipCp1zhJsXAFUKuqEfLngXodOvC4tAOr76h11S57o5lN4506Poq2mWgAZe/\n" +
"JZr9MEn1+w==\n" +
"-----END CERTIFICATE-----\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIFnzCCA4egAwIBAgIUcUNMqgWoDLsvMj0YmEudj60EG5swDQYJKoZIhvcNAQEL\n" +
"BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG\n" +
"A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj\n" +
"aGUwIBcNMjUwNjE2MTAyNzM2WhgPMjEyNTA1MjMxMDI3MzZaMF4xCzAJBgNVBAYT\n" +
"AlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoMCkNsb3VkU3Rh\n" +
"Y2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMIICIjANBgkqhkiG\n" +
"9w0BAQEFAAOCAg8AMIICCgKCAgEAwVQaePulUM523gKw168ToYp+gt05bXbu4Gg8\n" +
"uaRDKhnRAX1sEgYwkQ36Q+iTDEM9sKRma8lMNMIqkZMQdk6sIGX6BL+6wUOb7mL0\n" +
"5+I0yO9i8ooaGgNaeNvZftNIRlLsnPMGJaeom2/66XV4CsMqoZKaJ1H/I8N+bAeD\n" +
"GvrBx+B4l9D3G390nQvot9JUzrJgGuLl0KDHapvhlR39cCgEfIii02uX1iy0qXlV\n" +
"b+G1kLvpeC7T+lsJxondPJ69aO3lbDv/izyWw7qqBC57UhT/oKDxJmjQqklqzhgt\n" +
"nM/p3YE7M0nkRi3LnRmsZBz7o1DRf+M29zypKzXVk1aJflL46AtLMmpDIzVrEB2M\n" +
"q7o47rstXusYRYsBCqGTgdI1fV/CkDsZY5XkPZh2dsjZCHIS4P03OqFGsc6PQha2\n" +
"+y2AhV1pvywkDl48kPKSukHfV1RtaPZUZtcQKztwHH+aFfo9mD8z0H2HcExdXKzd\n" +
"jhRhI9ZSwFj3HEN9f5P8fS3lf5+fV7EEbG4NisieBj/UivW6QiTHpLD7wRLIUt2g\n" +
"XgXNF0lfJzYHbIcxQ6kfC5McU2fu6mUC+p/pNN8G0POS3S2T55tEUqLL4N0SadQy\n" +
"N1TZlTd2xTn+Hb6WlG0f5m97xGcNlGHKBvntFrHvOIfkEQ9ne3MlOO1Gjlintowo\n" +
"fRGf15kCAwEAAaNTMFEwHQYDVR0OBBYEFM4WEQJpN9M07Q8CHq+5owG93Dj8MB8G\n" +
"A1UdIwQYMBaAFM4WEQJpN9M07Q8CHq+5owG93Dj8MA8GA1UdEwEB/wQFMAMBAf8w\n" +
"DQYJKoZIhvcNAQELBQADggIBABr5RKGc/rKx4fOgyXNJR4aCJCTtPZKs4AUCCBYz\n" +
"cWOjJYGNvThOPSVx51nAD8+YP2BwkOIPfhl2u/+vQSSbz4OlauXLki2DUN8E2OFe\n" +
"gJfxPDzWOfAo/QOJcyHwSlnIQjiZzG2lK3eyf5IFnfQILQzDvXaUYvMBkl2hb5Q7\n" +
"44H6tRw78uuf/KsT4rY0bBFMN5DayjiyvoIDUvzCRqcb2KOi9DnZ7pXjduL7tO0j\n" +
"PhlQ24B77LVUUAvydIGUzmbhGC2VvY1qE7uaYgYtgSUZ0zSjJrHjUjVLMzRouNP7\n" +
"jpbBQRAcP4FDcOFZBHogunA0hxQdm0d8u3LqDYPNS0rpfW0ddU/72nfBX4bnoDEN\n" +
"+anw4wOgFuUcoEThALWZ9ESVKxXQ9Fpvd6FRW8fLLqhXAuli1BqP1c1WRxagldYe\n" +
"nPGm/FGZyJ2xOak9Uigi9NAQ/vX6CEfgcJgFZmCo8EKH0d4Ut72vGUcPqiUhT2EI\n" +
"AFAd6drSyoUdXXniSMWky9Vrt+qtLuAD1nhHTv8ZPdItXokoiD6ea/4xrbUZn0qY\n" +
"lLMDyfY76UVF0ruTR2Q6IdSq/zSggdwgkTooOW4XZcRf5l/ZnoeVQ1QH9C85SIKH\n" +
"IKZwPeGUm+EntmpuCBDmQSHLRCGEThd64iOAjqLR6arLj4TBJzBrZsGHFJbm0OcI\n" +
"dwa9\n" +
"-----END CERTIFICATE-----";
final CertServiceImpl certService = new CertServiceImpl();
certService.validate(certificate, key, null, certChains, false);
}
} }

View File

@ -16,6 +16,7 @@
# under the License. # under the License.
import logging import logging
import os.path import os.path
from os import listdir
import re import re
from cs.CsDatabag import CsDataBag from cs.CsDatabag import CsDataBag
from .CsProcess import CsProcess from .CsProcess import CsProcess
@ -25,6 +26,7 @@ from . import CsHelper
HAPROXY_CONF_T = "/etc/haproxy/haproxy.cfg.new" HAPROXY_CONF_T = "/etc/haproxy/haproxy.cfg.new"
HAPROXY_CONF_P = "/etc/haproxy/haproxy.cfg" HAPROXY_CONF_P = "/etc/haproxy/haproxy.cfg"
SSL_CERTS_DIR = "/etc/cloudstack/ssl/"
class CsLoadBalancer(CsDataBag): class CsLoadBalancer(CsDataBag):
""" Manage Load Balancer entries """ """ Manage Load Balancer entries """
@ -34,6 +36,9 @@ class CsLoadBalancer(CsDataBag):
return return
if 'configuration' not in list(self.dbag['config'][0].keys()): if 'configuration' not in list(self.dbag['config'][0].keys()):
return return
if 'ssl_certs' in list(self.dbag['config'][0].keys()):
self._create_pem_for_sslcert(self.dbag['config'][0]['ssl_certs'])
config = self.dbag['config'][0]['configuration'] config = self.dbag['config'][0]['configuration']
file1 = CsFile(HAPROXY_CONF_T) file1 = CsFile(HAPROXY_CONF_T)
file1.empty() file1.empty()
@ -43,6 +48,11 @@ class CsLoadBalancer(CsDataBag):
file1.commit() file1.commit()
file2 = CsFile(HAPROXY_CONF_P) file2 = CsFile(HAPROXY_CONF_P)
if not file2.compare(file1): if not file2.compare(file1):
# Verify new haproxy config before haproxy restart/reload
haproxy_err = self._verify_haproxy_config(HAPROXY_CONF_T)
if haproxy_err:
raise Exception("haproxy config is invalid with error \n%s" % haproxy_err)
CsHelper.copy(HAPROXY_CONF_T, HAPROXY_CONF_P) CsHelper.copy(HAPROXY_CONF_T, HAPROXY_CONF_P)
proc = CsProcess(['/run/haproxy.pid']) proc = CsProcess(['/run/haproxy.pid'])
@ -82,3 +92,29 @@ class CsLoadBalancer(CsDataBag):
ip = path[0] ip = path[0]
port = path[1] port = path[1]
firewall.append(["filter", "", "-A INPUT -p tcp -m tcp -d %s --dport %s -m state --state NEW -j ACCEPT" % (ip, port)]) firewall.append(["filter", "", "-A INPUT -p tcp -m tcp -d %s --dport %s -m state --state NEW -j ACCEPT" % (ip, port)])
def _create_pem_for_sslcert(self, ssl_certs):
logging.debug("CsLoadBalancer:: creating new pem files in %s and cleaning up it" % SSL_CERTS_DIR)
if not os.path.exists(SSL_CERTS_DIR):
CsHelper.execute("mkdir -p %s" % SSL_CERTS_DIR)
cert_names = []
for cert in ssl_certs:
cert_names.append(cert['name'] + ".pem")
file = CsFile("%s/%s.pem" % (SSL_CERTS_DIR, cert['name']))
file.empty()
file.add("%s\n" % cert['cert'].replace("\r\n", "\n"))
if 'chain' in cert.keys():
file.add("%s\n" % cert['chain'].replace("\r\n", "\n"))
file.add("%s\n" % cert['key'].replace("\r\n", "\n"))
file.commit()
for f in listdir(SSL_CERTS_DIR):
if f not in cert_names:
CsHelper.execute("rm -rf %s/%s" % (SSL_CERTS_DIR, f))
def _verify_haproxy_config(self, config):
ret = CsHelper.execute2("haproxy -c -f %s" % config)
if ret.returncode:
stdout, stderr = ret.communicate()
logging.error("haproxy config is invalid with error: %s" % stderr)
return stderr
return ""

View File

@ -0,0 +1,568 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from marvin.codes import FAILED
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import wait_until
from marvin.lib.base import (Account,
Project,
UserData,
SslCertificate,
Template,
NetworkOffering,
ServiceOffering,
VirtualMachine,
Network,
VPC,
VpcOffering,
PublicIPAddress,
LoadBalancerRule)
from marvin.lib.common import (get_domain, get_zone, get_test_template)
from nose.plugins.attrib import attr
import os
import subprocess
import logging
_multiprocess_shared_ = True
DOMAIN = "test-ssl-offloading.cloudstack.org"
CONTENT = "Test page"
FULL_CHAIN = "/tmp/full_chain.crt"
CERT = {
"privatekey": """-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCph7jsoMCQirRn
3obuvgnnefTXRQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0
rXjjgsVT3r4bv+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRx
kB0klwUcj/jk/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0q
t6KDMkUwv8fyzrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaM
Pe7eqSFzxunF9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXq
HNUwYkALAgMBAAECggEAK5JiiQ7X7053B6s96uaVDRVfRGTNKa5iMXBNDHq3wbHZ
X4IJAVr+PE7ivxdKco3r45fT11X9ZpUsssdTJsZZiTDak69BTiFcaaRCnmqOIlpd
J7vb6TMrTIW8RvxQ0M/txm6DuNHLibqJX5a2pszZ13l5cwECfF9/v/XLJTTukCbu
6D/f3fBVFl1tM8y9saOEYLkdb4dILWY61bVSDNswgprz2EV1SFnk5jxz2FuBrM/Q
+7hINvjDcaRvcm59hRb1rkljv7S10VoNw/CFkU451csJkUe4vWZwB8lZK/XxLQG0
HEdS1zU1XY8H8Y1RCrxjGRyiiWsBtUThhWYlPrGCoQKBgQDkP09YAlKqXhT69Kx5
keg2i1jV2hA73zWbWXt9xp5jG5r3pl3m170DvKL93YIDnHtpTC56mlzGrzS7DSTN
p0buY9Qb3fkJxunCpPVFo0HMFkpeR77ax0v34NzSohlRLKFo5R2M1cmDfbVbnSSl
MB57FfRRMxzjrk+dJvjOeJsxjwKBgQC+JLb4B8CZjpurXYg3ySiRqFsCqkqob+kf
9dR+rWvcR6vMTEyha0hUlDvTikDepU2smYR4oPHfdcXF9lAJ7T02UmQDeizAqR68
u9e+yS0q3tdRnPPZmXJfaDCXG1hKMqF4YA5Vs0XAjleF3zHB+vBLrnlPpShtd/Mu
sWTpxICTxQKBgQDSr/n+pE5IQwYczOO0aFGwn5pF9L9NdPHXz5aleETV+TJn7WL6
ZiRsoaDWs7SCvtxQS2kP9RM0t5/2FeDmEMXx4aZ2fsSWGM3IxVo+iL+Aswa81n8/
Ff5y9lb/+29hNdBcsjk/ukwEG3Lf+UNNVAie15oppgPByzJkPwgmFsAy0wKBgHDX
/TZp82WuerhSw/rHiSoYjhqg0bnw4Ju1Gy0q4q5SYqTWS0wpDT4U0wSSMjlwRQ6/
9RxZ9/G0RXFc4tdhUkig0PY3VcPpGnLL0BhL8GBW69ZlnVpwdK4meV/UPKucLLPx
3dACmszSLSMn+LG0qVNg8mHQFJQS8eGuKcOKePw5AoGACuxtefROKdKOALh4lTi2
VOwPZ+1jxsm6lKNccIEvbUpe3UXPgNWpJiDX8mUcob4/NBLzmV3BUVKbG7Exbo5J
LoMfp7OsztWUFwt7YAvRfS8fHdhkEsxEf3T72ADieH5ZAuXFF+K0H3r6HtWPD4ws
mTJjGP4+Bl/dFakA5FJcjHg=
-----END PRIVATE KEY-----""",
"certificate": """-----BEGIN CERTIFICATE-----
MIIFKjCCAxKgAwIBAgIUJ7BtN56KI8OuzbbM8SdtCLCB2UgwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG
A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj
aGUwHhcNMjUwNjIzMTMxMzA3WhcNMzUwNjIxMTMxMzA3WjBoMQswCQYDVQQGEwJY
WDELMAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMQ8wDQYDVQQKDAZBcGFjaGUxEzAR
BgNVBAsMCkNsb3VkU3RhY2sxGTAXBgNVBAMMECouY2xvdWRzdGFjay5vcmcwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCph7jsoMCQirRn3obuvgnnefTX
RQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0rXjjgsVT3r4b
v+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRxkB0klwUcj/jk
/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0qt6KDMkUwv8fy
zrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaMPe7eqSFzxunF
9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXqHNUwYkALAgMB
AAGjgdUwgdIwKwYDVR0RBCQwIoIQKi5jbG91ZHN0YWNrLm9yZ4IOY2xvdWRzdGFj
ay5vcmcwHQYDVR0OBBYEFCcq7jrdsqTD+Xi85DCqjYdL1gOqMIGDBgNVHSMEfDB6
oWKkYDBeMQswCQYDVQQGEwJYWDELMAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMw
EQYDVQQKDApDbG91ZFN0YWNrMQ8wDQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFw
YWNoZYIURVB9+qvRJyOnJnqmYOw467vW3vQwDQYJKoZIhvcNAQELBQADggIBACld
lEXgn/A4/kZQbLwwMxBvaoPDDaDaYVpPbOoPw7a8YkrL0rmPIc04PyX9GAqxdC+c
qaEXvmp3I+BdT13XGcBosXO8uEQ3kses9F3MhOHORPS2mJag7t4eLnNX/0CgKTlR
6yC2Gu7d3xPNJ+CKMxekdoF31StEFNAYI/La/q3D+IGsRCbrVu3xpPaw2XlXI7Ro
RU7yebVmQPSNc75bm8Ydo1cdYtz9h8PVnc+6ThhSrdS3jYScj9DrX5ZJaKuZqSlu
0ZqFXoBflme+cYB7nb9HqnIO67r9vzd2dTcErJVAk5jQqG5Y38d1tingDx1A5opU
z4BkXEbHNV6VXYUQ5VE0dXO2sNvXVJrstwMPE8d3EvbX/1gWj8kuymbskrCjySE4
4Yztkb0dsJkVU793lz3EV75DsXvj3gevK049nPv2Grt1+rTgFNa6NJnLvKIKk/mv
fWjxbK2b/AAJ1ci6xtw/vKmIWoEu6uEMIJmhfBwuP+VnVJWJbmYXpNW/L5g21B76
Fn8RuQa3mlm5lZrxEcJ/b6fF+2NPJwj7sh6l688VtNXoVSSyXUeV5HwqCv+YMjKn
CtwpEN/eNHMbrkJvgYwSoOzqhV/wpmNi28S7MOm66JMECHOXOhk/eX2chIEjiVna
MXhvr/Twfj2N4gNVtcgXkrk39HEYjk5+uF7SdNf4
-----END CERTIFICATE-----""",
"certchain": """-----BEGIN CERTIFICATE-----
MIIFQzCCAysCFEVQffqr0ScjpyZ6pmDsOOu71t70MA0GCSqGSIb3DQEBCwUAMF4x
CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM
CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMB4X
DTI1MDYxNjEwMjc1NloXDTMwMDYxNTEwMjc1NlowXjELMAkGA1UEBhMCWFgxCzAJ
BgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEGA1UECgwKQ2xvdWRTdGFjazEPMA0G
A1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFjaGUwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQCLiQmSjrht15R1F+r79m/LZN5hsfQBGp+dy+yrtsWfOOur
RdXAwgbLxxsyKMQKWCQxlRI7wdhqh0L0ZBrIr9MjltYqsqLAoLmgY4eG/f6G8YGr
O/rxzfwTLbCeaIseF/OMA6Sz125HXYp1bltYK4LsuC7tihZXbeVa5pUGs3Jwgcfx
LYm4eB42Hp7Eg05uL8LbwT/1AjcwoWkTewKAWXA83zgLRDFDbl1t0IPHI4cdVvia
BNwNbG49ZCF6OgmokSarQSe4Vbems1u9T9pAySXAVjEYBqFjKWyswpdr782uNLmB
lCGm0pDeJ9/WASxbTJr7k9H6ZpnaHr54DG6ZqennWMz8w6r2pf7bp/EGZ3mZQ4s3
5ylSP4cQt8CSSI8k2CflPGUyytUAiWlDS3qSyIuAOPKXDg7wIpcbwcu4VMeKnH0Z
x7Uu9j1UDZEZoSu6UI/VInTl47k1/ECD+AO9yBzZSv+pTQmO3/Im3CcxsTHmVd5s
Tl0CJ/jWNpo9DAMtmGvt6CBWBXGRsO2XNk7djRcq2CubiCpvODg+7CcR6CiZK73L
1aOisLiq3+ofiJSSXRRuKtJlkQ4eSPSbYWkNJcKmIhbCoYOdH/Pe3/+RHjvNc1kO
OUb+icmfzcMVAs3C5jybpazsfjDNQZXWAFx4FLDcqOVbrCwom+tMukw+hzlZnwID
AQABMA0GCSqGSIb3DQEBCwUAA4ICAQAdexoMwn+Ol1A3+NOHk9WJZX+t2Q8/9wWb
K+jSVleSfXXWsB1mC3fABVJQdCxtXCH3rpt4w7FK6aUes9VjqAHap4tt9WBq0Wqq
vvMURFHfllxEM31Z35kBOCSQY9xwpGV1rRh/zYs4h55YixomR3nXNZ9cI8xzlSCi
sMG0mv0y+yxPohKrZj3AzLYz/M11SimSoyRPIANI+cUg1nJXyQoHzVHWEp1Nj0HB
M/GW05cxsWea7f5YcAW1JQI3FOkpwb72fIZOtMDa4PO8IYWXJAeAc/chw745/MTi
Rvl2NT4RZBAcrSNbhCOzRPG/ZiG+ArQuCluZ9HHAXRBMTtlLk5DO4+XxZlyGpjwf
uKniK8dccy9uU0ho73p9SNDhXH0yb9Naj8vd9NWzCUYaaBXt/92cIyhaAHAVFxJu
o6jr2FLbnhSGF9EO/tHvF7LxZv1dnbInvlWHwoFQjwmoeB+e17lHBdPMnWnPKBZe
jA2VH/IzGCucWuWQhruummO5GT8Z6F4jBwvafBo+QARKPZgEBpx3LycXrpkYI3LT
GGOpGCxFt5tVZOEsC/jQ5rIljNSeTzWmzfNRn/yRUW97uWsrzcQIBAUtu/pQnyFQ
WCnC1ipCp1zhJsXAFUKuqEfLngXodOvC4tAOr76h11S57o5lN4506Poq2mWgAZe/
JZr9MEn1+w==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFnzCCA4egAwIBAgIUcUNMqgWoDLsvMj0YmEudj60EG5swDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG
A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj
aGUwIBcNMjUwNjE2MTAyNzM2WhgPMjEyNTA1MjMxMDI3MzZaMF4xCzAJBgNVBAYT
AlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoMCkNsb3VkU3Rh
Y2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMIICIjANBgkqhkiG
9w0BAQEFAAOCAg8AMIICCgKCAgEAwVQaePulUM523gKw168ToYp+gt05bXbu4Gg8
uaRDKhnRAX1sEgYwkQ36Q+iTDEM9sKRma8lMNMIqkZMQdk6sIGX6BL+6wUOb7mL0
5+I0yO9i8ooaGgNaeNvZftNIRlLsnPMGJaeom2/66XV4CsMqoZKaJ1H/I8N+bAeD
GvrBx+B4l9D3G390nQvot9JUzrJgGuLl0KDHapvhlR39cCgEfIii02uX1iy0qXlV
b+G1kLvpeC7T+lsJxondPJ69aO3lbDv/izyWw7qqBC57UhT/oKDxJmjQqklqzhgt
nM/p3YE7M0nkRi3LnRmsZBz7o1DRf+M29zypKzXVk1aJflL46AtLMmpDIzVrEB2M
q7o47rstXusYRYsBCqGTgdI1fV/CkDsZY5XkPZh2dsjZCHIS4P03OqFGsc6PQha2
+y2AhV1pvywkDl48kPKSukHfV1RtaPZUZtcQKztwHH+aFfo9mD8z0H2HcExdXKzd
jhRhI9ZSwFj3HEN9f5P8fS3lf5+fV7EEbG4NisieBj/UivW6QiTHpLD7wRLIUt2g
XgXNF0lfJzYHbIcxQ6kfC5McU2fu6mUC+p/pNN8G0POS3S2T55tEUqLL4N0SadQy
N1TZlTd2xTn+Hb6WlG0f5m97xGcNlGHKBvntFrHvOIfkEQ9ne3MlOO1Gjlintowo
fRGf15kCAwEAAaNTMFEwHQYDVR0OBBYEFM4WEQJpN9M07Q8CHq+5owG93Dj8MB8G
A1UdIwQYMBaAFM4WEQJpN9M07Q8CHq+5owG93Dj8MA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggIBABr5RKGc/rKx4fOgyXNJR4aCJCTtPZKs4AUCCBYz
cWOjJYGNvThOPSVx51nAD8+YP2BwkOIPfhl2u/+vQSSbz4OlauXLki2DUN8E2OFe
gJfxPDzWOfAo/QOJcyHwSlnIQjiZzG2lK3eyf5IFnfQILQzDvXaUYvMBkl2hb5Q7
44H6tRw78uuf/KsT4rY0bBFMN5DayjiyvoIDUvzCRqcb2KOi9DnZ7pXjduL7tO0j
PhlQ24B77LVUUAvydIGUzmbhGC2VvY1qE7uaYgYtgSUZ0zSjJrHjUjVLMzRouNP7
jpbBQRAcP4FDcOFZBHogunA0hxQdm0d8u3LqDYPNS0rpfW0ddU/72nfBX4bnoDEN
+anw4wOgFuUcoEThALWZ9ESVKxXQ9Fpvd6FRW8fLLqhXAuli1BqP1c1WRxagldYe
nPGm/FGZyJ2xOak9Uigi9NAQ/vX6CEfgcJgFZmCo8EKH0d4Ut72vGUcPqiUhT2EI
AFAd6drSyoUdXXniSMWky9Vrt+qtLuAD1nhHTv8ZPdItXokoiD6ea/4xrbUZn0qY
lLMDyfY76UVF0ruTR2Q6IdSq/zSggdwgkTooOW4XZcRf5l/ZnoeVQ1QH9C85SIKH
IKZwPeGUm+EntmpuCBDmQSHLRCGEThd64iOAjqLR6arLj4TBJzBrZsGHFJbm0OcI
dwa9
-----END CERTIFICATE-----""",
"enabledrevocationcheck": False
}
# Install apache2 via userdata
USER_DATA="""I2Nsb3VkLWNvbmZpZwpydW5jbWQ6CiAgLSBzdWRvIGFwdC1nZXQgdXBkYXRlCiAgLSBzdWRvIGFw
dC1nZXQgaW5zdGFsbCAteSBhcGFjaGUyCiAgLSBzdWRvIHN5c3RlbWN0bCBlbmFibGUgYXBhY2hl
MgogIC0gc3VkbyBzeXN0ZW1jdGwgc3RhcnQgYXBhY2hlMgogIC0gZWNobyAiVGVzdCBwYWdlIiB8
c3VkbyB0ZWUgL3Zhci93d3cvaHRtbC90ZXN0Lmh0bWwK"""
# #cloud-config
# runcmd:
# - sudo apt-get update
# - sudo apt-get install -y apache2
# - sudo systemctl enable apache2
# - sudo systemctl start apache2
# - echo "Test page" |sudo tee /var/www/html/test.html
class TestSslOffloading(cloudstackTestCase):
@classmethod
def setUpClass(cls):
testClient = super(TestSslOffloading, cls).getClsTestClient()
cls.apiclient = testClient.getApiClient()
cls.services = testClient.getParsedTestDataConfig()
cls._cleanup = []
# Get Zone, Domain and templates
cls.domain = get_domain(cls.apiclient)
cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests())
cls.hypervisor = testClient.getHypervisorInfo()
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
# Save full chain as a file
with open(FULL_CHAIN, "w", encoding="utf-8") as f:
f.write(CERT["certchain"])
# Register template if needed
if cls.hypervisor.lower() == 'simulator':
cls.template = get_test_template(
cls.apiclient,
cls.zone.id,
cls.hypervisor)
else:
cls.template = Template.register(
cls.apiclient,
cls.services["test_templates_cloud_init"][cls.hypervisor.lower()],
zoneid=cls.zone.id,
hypervisor=cls.hypervisor,
)
cls.template.download(cls.apiclient)
cls._cleanup.append(cls.template)
if cls.template == FAILED:
assert False, "get_test_template() failed to return template"
# Create service offering
cls.service_offering = ServiceOffering.create(
cls.apiclient,
cls.services["service_offerings"]["big"] # 512MB memory
)
cls._cleanup.append(cls.service_offering)
# Create network offering
cls.services["isolated_network_offering"]["egress_policy"] = "true"
cls.network_offering = NetworkOffering.create(cls.apiclient,
cls.services["isolated_network_offering"],
conservemode=True)
cls.network_offering.update(cls.apiclient, state='Enabled')
cls._cleanup.append(cls.network_offering)
#Create an account, network, VM and IP addresses
cls.account = Account.create(
cls.apiclient,
cls.services["account"],
admin=True,
domainid=cls.domain.id
)
cls._cleanup.append(cls.account)
cls.user = cls.account.user[0]
cls.userapiclient = cls.testClient.getUserApiClient(cls.user.username, cls.domain.name)
cls.logger = logging.getLogger("TestSslOffloading")
cls.stream_handler = logging.StreamHandler()
cls.logger.setLevel(logging.DEBUG)
cls.logger.addHandler(cls.stream_handler)
def setUp(self):
self.apiclient = self.testClient.getApiClient()
self.cleanup = []
def tearDown(self):
super(TestSslOffloading, self).tearDown()
@classmethod
def tearDownClass(cls):
super(TestSslOffloading, cls).tearDownClass()
# Remove full chain file
if os.path.exists(FULL_CHAIN):
os.remove(FULL_CHAIN)
def wait_for_service_ready(self, command, expected, retries=60):
output = None
self.logger.debug("======================================")
self.logger.debug("Checking output of command '%s', expected result: '%s'" % (command, expected))
def check_output():
try:
output = subprocess.check_output(command + ' 2>&1', shell=True).strip().decode('utf-8')
except Exception as e:
self.logger.debug("Failed to get output of command '%s': '%s'" % (command, e))
if expected is None:
self.logger.debug("But it is expected")
return True, None
return False, None
self.logger.debug("Output of command '%s' is '%s'" % (command, output))
if expected is None:
self.logger.debug("But it is expected to be None")
return False, None
return (expected in output), None
res, _ = wait_until(10, retries, check_output)
if not res:
self.fail("Failed to wait for http server to show content '%s'. The output is '%s'" % (expected, output))
@attr(tags = ["advanced", "advancedns", "smoke"], required_hardware="true")
def test_01_ssl_offloading_isolated_network(self):
"""Test to create Load balancing rule with SSL offloading"""
# Validate:
# 1. Create isolated network and vm instance
# 2. create LB with port 80 -> 80, verify the website (should get expected content)
# 3. create LB with port 443 -> 80, verify the website (should not work)
# 4. add cert to LB with port 443
# 5. verify the website (should get expected content)
# 6. remove cert from LB with port 443
# 7. delete SSL certificate
# Register Userdata
self.userdata = UserData.register(self.apiclient,
name="test-userdata",
userdata=USER_DATA,
account=self.account.name,
domainid=self.account.domainid
)
# Upload SSL Certificate
self.sslcert = SslCertificate.create(self.apiclient,
CERT,
name="test-ssl-certificate",
account=self.account.name,
domainid=self.account.domainid)
# 1. Create network
self.network = Network.create(self.apiclient,
zoneid=self.zone.id,
services=self.services["network"],
domainid=self.domain.id,
account=self.account.name,
networkofferingid=self.network_offering.id)
self.cleanup.append(self.network)
self.services["virtual_machine"]["networkids"] = [str(self.network.id)]
# Create vm instance
self.vm_1 = VirtualMachine.create(
self.apiclient,
self.services["virtual_machine"],
templateid=self.template.id,
accountid=self.account.name,
domainid=self.account.domainid,
userdataid=self.userdata.userdata.id,
serviceofferingid=self.service_offering.id
)
self.cleanup.append(self.vm_1)
self.public_ip = PublicIPAddress.create(
self.apiclient,
self.account.name,
self.zone.id,
self.account.domainid,
self.services["virtual_machine"],
self.network.id)
# 2. create LB with port 80 -> 80, verify the website (should get expected content).
# firewall is open by default
lb_http = {
"name": "http",
"alg": "roundrobin",
"privateport": 80,
"publicport": 80,
"protocol": "tcp"
}
lb_rule_http = LoadBalancerRule.create(
self.apiclient,
lb_http,
self.public_ip.ipaddress.id,
accountid=self.account.name,
domainid=self.domain.id,
networkid=self.network.id
)
lb_rule_http.assign(self.apiclient, [self.vm_1])
command = "curl -sL --connect-timeout 3 http://%s/test.html" % self.public_ip.ipaddress.ipaddress
# wait 10 minutes until the webpage is available. it returns "503 Service Unavailable" if not available
self.wait_for_service_ready(command, CONTENT, 60)
# 3. create LB with port 443 -> 80, verify the website (should not work)
# firewall is open by default
lb_https = {
"name": "https",
"alg": "roundrobin",
"privateport": 80,
"publicport": 443,
"protocol": "ssl"
}
lb_rule_https = LoadBalancerRule.create(
self.apiclient,
lb_https,
self.public_ip.ipaddress.id,
accountid=self.account.name,
domainid=self.domain.id,
networkid=self.network.id
)
lb_rule_https.assign(self.apiclient, [self.vm_1])
command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
# 4. add cert to LB with port 443
lb_rule_https.assignCert(self.apiclient, self.sslcert.id)
# 5. verify the website (should get expected content)
command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
command = "curl -sL --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, CONTENT, 1)
command = "curl -sL --connect-timeout 3 --cacert %s --resolve %s:443:%s https://%s/test.html" % (FULL_CHAIN, DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, CONTENT, 1)
# 6. remove cert from LB with port 443
lb_rule_https.removeCert(self.apiclient)
# 7. delete SSL certificate
self.sslcert.delete(self.apiclient)
@attr(tags = ["advanced", "advancedns", "smoke"], required_hardware="true")
def test_02_ssl_offloading_project_vpc(self):
"""Test to create Load balancing rule with SSL offloading in VPC in user project"""
# Validate:
# 1. Create VPC, VPC tier and vm instance
# 2. create LB with port 80 -> 80, verify the website (should get expected content)
# 3. create LB with port 443 -> 80, verify the website (should not work)
# 4. add cert to LB with port 443
# 5. verify the website (should get expected content)
# 6. remove cert from LB with port 443
# 7. delete SSL certificate
# Create project by user
self.project = Project.create(
self.userapiclient,
self.services["project"]
)
self.cleanup.append(self.project)
# Register Userdata by user
self.userdata = UserData.register(self.userapiclient,
name="test-user-userdata",
userdata=USER_DATA,
projectid=self.project.id
)
# Upload SSL Certificate by user
self.sslcert = SslCertificate.create(self.userapiclient,
CERT,
name="test-user-ssl-certificate",
projectid=self.project.id
)
# 1. Create VPC and VPC tier
vpcOffering = VpcOffering.list(self.userapiclient, name="Default VPC offering")
self.assertTrue(vpcOffering is not None and len(
vpcOffering) > 0, "No VPC offerings found")
self.vpc = VPC.create(
apiclient=self.userapiclient,
services=self.services["vpc_vpn"]["vpc"],
vpcofferingid=vpcOffering[0].id,
zoneid=self.zone.id,
projectid=self.project.id
)
self.cleanup.append(self.vpc)
networkOffering = NetworkOffering.list(
self.userapiclient, name="DefaultIsolatedNetworkOfferingForVpcNetworks")
self.assertTrue(networkOffering is not None and len(
networkOffering) > 0, "No VPC based network offering")
self.network = Network.create(
apiclient=self.userapiclient,
services=self.services["vpc_vpn"]["network_1"],
networkofferingid=networkOffering[0].id,
zoneid=self.zone.id,
vpcid=self.vpc.id,
projectid=self.project.id
)
self.cleanup.append(self.network)
self.services["virtual_machine"]["networkids"] = [str(self.network.id)]
# Create vm instance
self.vm_2 = VirtualMachine.create(
self.userapiclient,
self.services["virtual_machine"],
templateid=self.template.id,
userdataid=self.userdata.userdata.id,
serviceofferingid=self.service_offering.id,
projectid=self.project.id
)
self.cleanup.append(self.vm_2)
self.public_ip = PublicIPAddress.create(
self.userapiclient,
zoneid=self.zone.id,
services=self.services["virtual_machine"],
networkid=self.network.id,
vpcid=self.vpc.id,
projectid=self.project.id
)
# 2. create LB with port 80 -> 80, verify the website (should get expected content).
# firewall is open by default
lb_http = {
"name": "http",
"alg": "roundrobin",
"privateport": 80,
"publicport": 80,
"protocol": "tcp"
}
lb_rule_http = LoadBalancerRule.create(
self.userapiclient,
lb_http,
self.public_ip.ipaddress.id,
networkid=self.network.id,
projectid=self.project.id
)
lb_rule_http.assign(self.userapiclient, [self.vm_2])
command = "curl -sL --connect-timeout 3 http://%s/test.html" % self.public_ip.ipaddress.ipaddress
# wait 10 minutes until the webpage is available. it returns "503 Service Unavailable" if not available
self.wait_for_service_ready(command, CONTENT, 60)
# 3. create LB with port 443 -> 80, verify the website (should not work)
# firewall is open by default
lb_https = {
"name": "https",
"alg": "roundrobin",
"privateport": 80,
"publicport": 443,
"protocol": "ssl"
}
lb_rule_https = LoadBalancerRule.create(
self.userapiclient,
lb_https,
self.public_ip.ipaddress.id,
networkid=self.network.id,
projectid=self.project.id
)
lb_rule_https.assign(self.userapiclient, [self.vm_2])
command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
# 4. add cert to LB with port 443
lb_rule_https.assignCert(self.userapiclient, self.sslcert.id)
# 5. verify the website (should get expected content)
command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, None, 1)
command = "curl -sL --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, CONTENT, 1)
command = "curl -sL --connect-timeout 3 --cacert %s --resolve %s:443:%s https://%s/test.html" % (FULL_CHAIN, DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
self.wait_for_service_ready(command, CONTENT, 1)
# 6. remove cert from LB with port 443
lb_rule_https.removeCert(self.userapiclient)
# 7. delete SSL certificate
self.sslcert.delete(self.userapiclient)

View File

@ -164,9 +164,10 @@ class CSConnection(object):
''' '''
try: try:
response = requests.post(url, response = requests.post(url,
params=payload, data=payload,
cert=self.certPath, cert=self.certPath,
verify=self.httpsFlag) verify=self.httpsFlag)
self.logger.debug("=======Got POST response : %s=======" % response)
return response return response
except Exception as e: except Exception as e:
self.__lastError = e self.__lastError = e

View File

@ -1068,7 +1068,7 @@ test_data = {
"displaytext": "ubuntu 22.04 kvm", "displaytext": "ubuntu 22.04 kvm",
"format": "raw", "format": "raw",
"hypervisor": "kvm", "hypervisor": "kvm",
"ostype": "Other Linux (64-bit)", "ostype": "Ubuntu 22.04 LTS",
"url": "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img", "url": "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img",
"requireshvm": "True", "requireshvm": "True",
"ispublic": "True", "ispublic": "True",

View File

@ -3080,6 +3080,9 @@ class LoadBalancerRule:
if "openfirewall" in services: if "openfirewall" in services:
cmd.openfirewall = services["openfirewall"] cmd.openfirewall = services["openfirewall"]
if "protocol" in services:
cmd.protocol = services["protocol"]
if projectid: if projectid:
cmd.projectid = projectid cmd.projectid = projectid
@ -3188,6 +3191,22 @@ class LoadBalancerRule:
[setattr(cmd, k, v) for k, v in list(kwargs.items())] [setattr(cmd, k, v) for k, v in list(kwargs.items())]
return apiclient.listLoadBalancerRuleInstances(cmd) return apiclient.listLoadBalancerRuleInstances(cmd)
def assignCert(self, apiclient, certId, forced=None):
""""""
cmd = assignCertToLoadBalancer.assignCertToLoadBalancerCmd()
cmd.lbruleid = self.id
cmd.certid = certId
if forced is not None:
cmd.forced = forced
return apiclient.assignCertToLoadBalancer(cmd)
def removeCert(self, apiclient):
"""Removes a certificate from a load balancer rule"""
cmd = removeCertFromLoadBalancer.removeCertFromLoadBalancerCmd()
cmd.lbruleid = self.id
return apiclient.removeCertFromLoadBalancer(cmd)
class Cluster: class Cluster:
"""Manage Cluster life cycle""" """Manage Cluster life cycle"""
@ -8016,3 +8035,60 @@ class GpuDevice:
cmd.id = self.id cmd.id = self.id
[setattr(cmd, k, v) for k, v in list(kwargs.items())] [setattr(cmd, k, v) for k, v in list(kwargs.items())]
return (apiclient.updateGpuDevice(cmd)) return (apiclient.updateGpuDevice(cmd))
class SslCertificate:
def __init__(self, items):
self.__dict__.update(items)
@classmethod
def create(cls, apiclient, services, name, certificate=None, privatekey=None,
certchain=None, password=None, enabledrevocationcheck=None,
account=None, domainid=None, projectid=None):
"""Upload SSL certificate"""
cmd = uploadSslCert.uploadSslCertCmd()
cmd.name = name
if certificate:
cmd.certificate = certificate
elif "certificate" in services:
cmd.certificate = services["certificate"]
if privatekey:
cmd.privatekey = privatekey
elif "privatekey" in services:
cmd.privatekey = services["privatekey"]
if certchain:
cmd.certchain = certchain
elif "certchain" in services:
cmd.certchain = services["certchain"]
if password:
cmd.password = password
elif "password" in services:
cmd.password = services["password"]
if enabledrevocationcheck is not None:
cmd.enabledrevocationcheck = enabledrevocationcheck
elif "enabledrevocationcheck" in services:
cmd.enabledrevocationcheck = services["enabledrevocationcheck"]
if account:
cmd.account = account
if projectid:
cmd.projectid = projectid
if domainid:
cmd.domainid = domainid
return SslCertificate(apiclient.uploadSslCert(cmd, method='POST').__dict__)
def delete(self, apiclient):
"""Delete SSL Certificate"""
cmd = deleteSslCert.deleteSslCertCmd()
cmd.id = self.id
apiclient.deleteSslCert(cmd)

View File

@ -505,10 +505,12 @@
"label.category": "Category", "label.category": "Category",
"label.certchain": "Chain", "label.certchain": "Chain",
"label.certificate": "Certificate", "label.certificate": "Certificate",
"label.certificate.chain": "Certificate chain",
"label.certificate.upload": "Certificate uploaded.", "label.certificate.upload": "Certificate uploaded.",
"label.certificate.upload.failed": "Certificate upload failed", "label.certificate.upload.failed": "Certificate upload failed",
"label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check.", "label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check.",
"label.certificateid": "Certificate ID", "label.certificateid": "Certificate ID",
"label.certificates": "Certificates",
"label.chainsize": "Chain size", "label.chainsize": "Chain size",
"label.change": "Change", "label.change": "Change",
"label.change.affinity": "Change affinity", "label.change.affinity": "Change affinity",
@ -972,6 +974,7 @@
"label.enable.vpn": "Enable remote access VPN", "label.enable.vpn": "Enable remote access VPN",
"label.enable.webhook": "Enable Webhook", "label.enable.webhook": "Enable Webhook",
"label.enabled": "Enabled", "label.enabled": "Enabled",
"label.enabled.revocation.check": "Enables revocation checking for certificates",
"label.encrypt": "Encrypt", "label.encrypt": "Encrypt",
"label.encryptroot": "Encrypt Root Disk", "label.encryptroot": "Encrypt Root Disk",
"label.end": "End", "label.end": "End",
@ -1480,6 +1483,7 @@
"label.make.user.project.owner": "Make User project owner", "label.make.user.project.owner": "Make User project owner",
"label.makeredundant": "Make redundant", "label.makeredundant": "Make redundant",
"label.manage": "Manage", "label.manage": "Manage",
"label.manage.ssl.cert": "Manage SSL certificate",
"label.manage.vpn.user": "Manage VPN Users", "label.manage.vpn.user": "Manage VPN Users",
"label.managed.instances": "Managed Instances", "label.managed.instances": "Managed Instances",
"label.managed.volumes": "Managed Volumes", "label.managed.volumes": "Managed Volumes",
@ -2053,6 +2057,7 @@
"label.remove.vpc.offering": "Remove VPC Offering", "label.remove.vpc.offering": "Remove VPC Offering",
"label.removed": "Removed", "label.removed": "Removed",
"label.removing": "Removing", "label.removing": "Removing",
"label.replace": "Replace",
"label.replace.acl": "Replace ACL", "label.replace.acl": "Replace ACL",
"label.report.bug": "Ask a question or Report an issue", "label.report.bug": "Ask a question or Report an issue",
"label.request": "Request", "label.request": "Request",
@ -2312,6 +2317,7 @@
"label.uefi.supported": "UEFI supported", "label.uefi.supported": "UEFI supported",
"label.unregister.extension": "Unregister Extension", "label.unregister.extension": "Unregister Extension",
"label.usediops": "IOPS used", "label.usediops": "IOPS used",
"label.userdata": "User Data",
"label.user.data.id": "User Data ID", "label.user.data.id": "User Data ID",
"label.user.data.name": "User Data name", "label.user.data.name": "User Data name",
"label.user.data.details": "User Data details", "label.user.data.details": "User Data details",
@ -2327,6 +2333,8 @@
"label.ssh.port": "SSH port", "label.ssh.port": "SSH port",
"label.sshkeypair": "New SSH key pair", "label.sshkeypair": "New SSH key pair",
"label.sshkeypairs": "SSH key pairs", "label.sshkeypairs": "SSH key pairs",
"label.ssl": "SSL",
"label.sslcertificate": "SSL certificate",
"label.sslcertificates": "SSL certificates", "label.sslcertificates": "SSL certificates",
"label.sslverification": "SSL verification", "label.sslverification": "SSL verification",
"label.standard.us.keyboard": "Standard (US) keyboard", "label.standard.us.keyboard": "Standard (US) keyboard",
@ -2587,6 +2595,7 @@
"label.upload.icon": "Upload icon", "label.upload.icon": "Upload icon",
"label.upload.iso.from.local": "Upload ISO from local", "label.upload.iso.from.local": "Upload ISO from local",
"label.upload.resource.icon": "Upload icon", "label.upload.resource.icon": "Upload icon",
"label.upload.ssl.certificate": "Upload SSL cerficicate",
"label.upload.template.from.local": "Upload Template from local", "label.upload.template.from.local": "Upload Template from local",
"label.upload.volume": "Upload volume", "label.upload.volume": "Upload volume",
"label.upload.volume.from.local": "Upload Volume from local", "label.upload.volume.from.local": "Upload Volume from local",
@ -2996,6 +3005,8 @@
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule", "message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...", "message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
"message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule", "message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule",
"message.remove.sslcert.failed": "Failed to remove SSL certificate from load balancer",
"message.remove.sslcert.processing": "Removing SSL certificate from load balancer...",
"message.add.netris.controller": "Add Netris Provider", "message.add.netris.controller": "Add Netris Provider",
"message.add.nsx.controller": "Add NSX Provider", "message.add.nsx.controller": "Add NSX Provider",
"message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>", "message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>",
@ -3047,6 +3058,8 @@
"message.allowed": "Allowed", "message.allowed": "Allowed",
"message.alert.show.all.stats.data": "This may return a lot of data depending on VM statistics and retention settings", "message.alert.show.all.stats.data": "This may return a lot of data depending on VM statistics and retention settings",
"message.apply.success": "Apply Successfully", "message.apply.success": "Apply Successfully",
"message.assign.sslcert.failed": "Failed to assign SSL certificate",
"message.assign.sslcert.processing": "Assigning SSL certificate...",
"message.assign.instance.another": "Please specify the Account type, domain, Account name and Network (optional) of the new Account. <br> If the default NIC of the Instance is on a shared Network, CloudStack will check if the Network can be used by the new Account if you do not specify one Network. <br> If the default NIC of the Instance is on a isolated Network, and the new Account has more one isolated Networks, you should specify one.", "message.assign.instance.another": "Please specify the Account type, domain, Account name and Network (optional) of the new Account. <br> If the default NIC of the Instance is on a shared Network, CloudStack will check if the Network can be used by the new Account if you do not specify one Network. <br> If the default NIC of the Instance is on a isolated Network, and the new Account has more one isolated Networks, you should specify one.",
"message.assign.vm.failed": "Failed to assign Instance", "message.assign.vm.failed": "Failed to assign Instance",
"message.assign.vm.processing": "Assigning Instance...", "message.assign.vm.processing": "Assigning Instance...",
@ -3762,6 +3775,7 @@
"message.success.add.vpc.network": "Successfully added a VPC network", "message.success.add.vpc.network": "Successfully added a VPC network",
"message.success.add.vpn.customer.gateway": "Successfully added VPN customer gateway", "message.success.add.vpn.customer.gateway": "Successfully added VPN customer gateway",
"message.success.add.vpn.gateway": "Successfully added VPN gateway", "message.success.add.vpn.gateway": "Successfully added VPN gateway",
"message.success.assign.sslcert": "Successfully assigned SSL certificate",
"message.success.assign.vm": "Successfully assigned Instance", "message.success.assign.vm": "Successfully assigned Instance",
"message.success.apply.network.policy": "Successfully applied Network Policy", "message.success.apply.network.policy": "Successfully applied Network Policy",
"message.success.apply.tungsten.tag": "Successfully applied Tag", "message.success.apply.tungsten.tag": "Successfully applied Tag",
@ -3840,6 +3854,7 @@
"message.success.release.ip": "Successfully released IP", "message.success.release.ip": "Successfully released IP",
"message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer", "message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer",
"message.success.release.dedicated.ipv4.subnet": "Successfully released dedicated IPv4 subnet", "message.success.release.dedicated.ipv4.subnet": "Successfully released dedicated IPv4 subnet",
"message.success.remove.sslcert": "Successfully removed SSL certificate from load balancer",
"message.success.remove.egress.rule": "Successfully removed egress rule", "message.success.remove.egress.rule": "Successfully removed egress rule",
"message.success.remove.objectstore.objects": "Successfully removed selected object(s)", "message.success.remove.objectstore.objects": "Successfully removed selected object(s)",
"message.success.remove.objectstore.directory": "Successfully removed selected directory", "message.success.remove.objectstore.directory": "Successfully removed selected directory",
@ -3888,6 +3903,7 @@
"message.success.upload.description": "This ISO file has been uploaded. Please check its status in the Templates menu.", "message.success.upload.description": "This ISO file has been uploaded. Please check its status in the Templates menu.",
"message.success.upload.icon": "Successfully uploaded icon for ", "message.success.upload.icon": "Successfully uploaded icon for ",
"message.success.upload.iso.description": "This ISO file has been uploaded. Please check its status in the images > ISOs menu.", "message.success.upload.iso.description": "This ISO file has been uploaded. Please check its status in the images > ISOs menu.",
"message.success.upload.ssl.cert": "Successfully uploaded SSL certificate",
"message.success.upload.template.description": "This Template file has been uploaded. Please check its status in the Templates menu.", "message.success.upload.template.description": "This Template file has been uploaded. Please check its status in the Templates menu.",
"message.success.upload.volume.description": "This volume has been uploaded. Please check its status in the volumes menu.", "message.success.upload.volume.description": "This volume has been uploaded. Please check its status in the volumes menu.",
"message.suspend.project": "Are you sure you want to suspend this project?", "message.suspend.project": "Are you sure you want to suspend this project?",

View File

@ -85,7 +85,7 @@ export default {
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue'))) component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
}, },
{ {
name: 'certificate', name: 'certificates',
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue'))) component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue')))
}, },
{ {

View File

@ -46,6 +46,10 @@ export default {
'listProjectRoles' in store.getters.apis 'listProjectRoles' in store.getters.apis
} }
}, },
{
name: 'certificates',
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue')))
},
{ {
name: 'limits', name: 'limits',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue'))) component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue')))

View File

@ -284,6 +284,7 @@
<a-select-option value="tcp-proxy">{{ $t('label.tcp.proxy') }}</a-select-option> <a-select-option value="tcp-proxy">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp">{{ $t('label.tcp') }}</a-select-option> <a-select-option value="tcp">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp">{{ $t('label.udp') }}</a-select-option> <a-select-option value="udp">{{ $t('label.udp') }}</a-select-option>
<a-select-option value="ssl">{{ $t('label.ssl') }}</a-select-option>
</a-select> </a-select>
</div> </div>
<div :span="24" class="action-button"> <div :span="24" class="action-button">

View File

@ -17,6 +17,17 @@
<template> <template>
<div> <div>
<a-row :gutter="12">
<a-spin :spinning="loading">
<a-button
shape="round"
style="left: 10px; float: right;margin-bottom: 10px; z-index: 8"
@click="() => { showUploadForm = true }">
<template #icon><plus-outlined /></template>
{{ $t('label.upload.ssl.certificate') }}
</a-button>
</a-spin>
</a-row>
<a-row :gutter="12"> <a-row :gutter="12">
<a-col :md="24" :lg="24"> <a-col :md="24" :lg="24">
<a-table <a-table
@ -68,16 +79,120 @@
</a-list> </a-list>
</a-col> </a-col>
</a-row> </a-row>
<a-modal
v-if="showUploadForm"
:visible="showUploadForm"
:title="$t('label.upload.ssl.certificate')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="() => { showUploadForm = false }"
centered
width="30vw">
<a-form
layout="vertical"
:ref="formRef"
:model="form"
:rules="rules"
@finish="uploadSslCert"
v-ctrl-enter="uploadSslCert"
>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description" tooltipPlacement="bottom"/>
</template>
<a-input
id="name"
:placeholder="apiParams.name.description"
name="name"
v-model:value="form.name"
></a-input>
</a-form-item>
<a-form-item name="certificate" ref="certificate" :required="true">
<template #label>
<tooltip-label :title="$t('label.certificate')" :tooltip="apiParams.certificate.description" tooltipPlacement="bottom"/>
</template>
<a-textarea
id="certificate"
rows="2"
:placeholder="apiParams.certificate.description"
v-focus="true"
name="certificate"
v-model:value="form.certificate"
></a-textarea>
</a-form-item>
<a-form-item name="privatekey" ref="privatekey" :required="true">
<template #label>
<tooltip-label :title="$t('label.privatekey')" :tooltip="apiParams.privatekey.description" tooltipPlacement="bottom"/>
</template>
<a-textarea
id="privatekey"
rows="2"
:placeholder="apiParams.privatekey.description"
name="privatekey"
v-model:value="form.privatekey"
></a-textarea>
</a-form-item>
<a-form-item name="certchain" ref="certchain">
<template #label>
<tooltip-label :title="$t('label.certificate.chain')" :tooltip="apiParams.certchain.description" tooltipPlacement="bottom"/>
</template>
<a-textarea
id="certchain"
rows="2"
:placeholder="apiParams.certchain.description"
name="certchain"
v-model:value="form.certchain"
></a-textarea>
</a-form-item>
<a-form-item name="password" ref="password">
<template #label>
<tooltip-label :title="$t('label.password')" :tooltip="apiParams.password.description" tooltipPlacement="bottom"/>
</template>
<a-input
type="password"
id="password"
name="password"
v-model:value="form.password"
></a-input>
</a-form-item>
<a-form-item name="enabledrevocationcheck" ref="enabledrevocationcheck">
<template #label>
<tooltip-label :title="$t('label.enabled.revocation.check')" :tooltip="apiParams.enabledrevocationcheck.description" tooltipPlacement="bottom"/>
</template>
<a-checkbox v-model:checked="form.enabledrevocationcheck"></a-checkbox>
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="showUploadForm = false" class="close-button">
{{ $t('label.cancel' ) }}
</a-button>
<a-button type="primary" ref="submit" :loading="uploading" @click="uploadSslCert">
{{ $t('label.submit' ) }}
</a-button>
</div>
</a-form>
</a-modal>
</div> </div>
</template> </template>
<script> <script>
import { getAPI, postAPI } from '@/api' import { getAPI, postAPI } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton' import TooltipButton from '@/components/widgets/TooltipButton'
import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
import { ref, reactive, toRaw } from 'vue'
export default { export default {
name: 'SSLCertificate', name: 'SSLCertificate',
components: { components: {
TooltipLabel,
TooltipButton TooltipButton
}, },
data () { data () {
@ -90,7 +205,9 @@ export default {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
quickview: false, quickview: false,
loading: false loading: false,
uploading: false,
showUploadForm: false
} }
}, },
props: { props: {
@ -127,6 +244,9 @@ export default {
} }
} }
}, },
beforeCreate () {
this.apiParams = this.$getApiParams('uploadSslCert')
},
created () { created () {
this.columns = [ this.columns = [
{ {
@ -149,14 +269,28 @@ export default {
} }
] ]
this.detailColumn = ['name', 'certificate', 'certchain'] this.detailColumn = ['name', 'certificate', 'certchain']
this.initForm()
this.fetchData() this.fetchData()
}, },
methods: { methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
certificate: [{ required: true, message: this.$t('label.required') }],
privatekey: [{ required: true, message: this.$t('label.required') }]
})
},
fetchData () { fetchData () {
const params = {} const params = {}
params.page = this.page params.page = this.page
params.pageSize = this.pageSize params.pageSize = this.pageSize
params.accountid = this.resource.id if (this.$route.meta.name === 'account') {
params.accountid = this.resource.id
delete params.projectid
} else { // project
params.projectid = this.resource.id
}
this.loading = true this.loading = true
@ -224,6 +358,46 @@ export default {
self.onDelete(row) self.onDelete(row)
} }
}) })
},
uploadSslCert () {
if (this.uploading) return
this.formRef.value.validate().then(() => {
const formValues = toRaw(this.form)
this.uploading = true
const params = {
name: formValues.name,
certificate: formValues.certificate,
privatekey: formValues.privatekey
}
if (formValues.enabledrevocationcheck != null && formValues.enabledrevocationcheck) {
params.enabledrevocationcheck = 'true'
} else {
params.enabledrevocationcheck = 'false'
}
if (this.$route.meta.name === 'account') {
params.account = this.resource.name
params.domainid = this.resource.domainid
} else { // project
params.projectid = this.resource.id
}
if (formValues.password) {
params.password = formValues.password
}
if (formValues.certchain) {
params.certchain = formValues.certchain
}
postAPI('uploadSslCert', params).then(json => {
this.$notification.success({
message: this.$t('message.success.upload.ssl.cert')
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.fetchData()
this.uploading = false
this.showUploadForm = false
})
})
} }
} }
} }

View File

@ -67,6 +67,7 @@
<a-select-option v-if="lbProvider !== 'Netris'" value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option> <a-select-option v-if="lbProvider !== 'Netris'" value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option> <a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option> <a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option>
<a-select-option value="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select> </a-select>
</div> </div>
<div class="form__item"> <div class="form__item">
@ -84,6 +85,12 @@
<a-select-option value="no">{{ $t('label.no') }}</a-select-option> <a-select-option value="no">{{ $t('label.no') }}</a-select-option>
</a-select> </a-select>
</div> </div>
<div class="form__item" v-if="newRule.protocol === 'ssl'" >
<div class="form__label">{{ $t('label.sslcertificate') }}</div>
<a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddSslCertModal(null)">
{{ this.selectedSsl.id != null ? this.selectedSsl.name : $t('label.add') }}
</a-button>
</div>
<div class="form__item" v-if="!newRule.autoscale || newRule.autoscale === 'no'"> <div class="form__item" v-if="!newRule.autoscale || newRule.autoscale === 'no'">
<div class="form__label" style="white-space: nowrap;">{{ $t('label.add.vms') }}</div> <div class="form__label" style="white-space: nowrap;">{{ $t('label.add.vms') }}</div>
<a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddVMModal"> <a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddVMModal">
@ -139,6 +146,12 @@
{{ returnStickinessLabel(record.id) }} {{ returnStickinessLabel(record.id) }}
</a-button> </a-button>
</template> </template>
<template v-if="column.key === 'sslcert'">
<a-button :disabled="record.protocol !== 'ssl'" @click="() => { selectedRule = record; handleOpenAddSslCertModal(record) }">
<template #icon><plus-outlined /></template>
{{ $t('label.manage') }}
</a-button>
</template>
<template v-if="column.key === 'autoscale'"> <template v-if="column.key === 'autoscale'">
<div> <div>
<router-link :to="{ path: '/autoscalevmgroup/' + record.autoscalevmgroup.id }" v-if='record.autoscalevmgroup'> <router-link :to="{ path: '/autoscalevmgroup/' + record.autoscalevmgroup.id }" v-if='record.autoscalevmgroup'>
@ -435,6 +448,7 @@
<a-select-option value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option> <a-select-option value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option> <a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option> <a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option>
<a-select-option value="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select> </a-select>
</div> </div>
<div :span="24" class="action-button"> <div :span="24" class="action-button">
@ -552,6 +566,60 @@
</div> </div>
</a-modal> </a-modal>
<a-modal
:title="$t('label.manage.ssl.cert')"
:maskClosable="false"
:closable="true"
v-if="addSslCertModalVisible"
:visible="addSslCertModalVisible"
width="30vw"
@cancel="addSslCertModalVisible = false"
@ok="addSslCertModalVisible = false"
:cancelButtonProps="{ style: { display: 'none' } }"
>
<a-row v-show="showAssignedSsl && assignedSslCert !== 'None'">
<a-col :span="8">
<div class="form__label">{{ $t("label.current") + ' ' + $t('label.sslcertificate') }}</div>
</a-col>
<a-col :span="10">
<div>{{ assignedSslCert }}</div>
</a-col>
<a-col :span="6">
<a-button :disabled="!deleteSslButtonVisible" type="danger" @click="removeSslFromLbRule()">
<template #icon><delete-outlined /></template>
{{ $t('label.remove') }}
</a-button>
</a-col>
</a-row>
<a-row style="margin-top: 16px">
<a-col :span="8">
<div class="form__label">{{ $t("label.new") + ' ' + $t('label.sslcertificate') }}</div>
</a-col>
<a-col :span="10">
<div class="form__item">
<a-select v-model:value="selectedSsl.name" style="width: 80%;" @change="selectssl">
<a-select-option
v-for="sslcert in sslcerts.data"
:key="sslcert.id">{{ sslcert.name }}
</a-select-option>
</a-select>
</div>
</a-col>
<a-col :span="6">
<div>
<a-button v-show="addSslButtonVisible && assignedSslCert !== 'None'" type="primary" @click="addSslTolbRule()">
<template #icon><swap-outlined /></template>
{{ $t('label.replace') }}
</a-button>
<a-button v-show="addSslButtonVisible && assignedSslCert === 'None'" type="primary" @click="addSslTolbRule()">
<template #icon><plus-outlined /></template>
{{ $t('label.assign') }}
</a-button>
</div>
</a-col>
</a-row>
</a-modal>
<a-modal <a-modal
:title="$t('label.select.tier')" :title="$t('label.select.tier')"
:maskClosable="false" :maskClosable="false"
@ -791,6 +859,20 @@ export default {
totalCount: 0, totalCount: 0,
page: 1, page: 1,
pageSize: 10, pageSize: 10,
sslcerts: {
loading: false,
data: []
},
selectedSsl: {
name: '',
id: null
},
addSslCertModalVisible: false,
showAssignedSsl: false,
currentAccountId: null,
assignedSslCert: 'None',
deleteSslButtonVisible: true,
addSslButtonVisible: true,
columns: [ columns: [
{ {
title: this.$t('label.name'), title: this.$t('label.name'),
@ -824,6 +906,10 @@ export default {
key: 'add', key: 'add',
title: this.$t('label.add.vms') title: this.$t('label.add.vms')
}, },
{
key: 'sslcert',
title: this.$t('label.sslcertificate')
},
{ {
key: 'autoscale', key: 'autoscale',
title: this.$t('label.autoscale') title: this.$t('label.autoscale')
@ -1092,6 +1178,147 @@ export default {
} }
}) })
}, },
fetchSslCerts () {
this.sslcerts.loading = true
this.sslcerts.data = []
// First get the account id
getAPI('listAccounts', {
name: this.resource.account,
domainid: this.resource.domainid
}).then(json => {
const accounts = json.listaccountsresponse.account || []
if (accounts.length > 0) {
// Now fetch all the ssl certs for this account
this.currentAccountId = accounts[0].id
getAPI('listSslCerts', {
accountid: this.currentAccountId
}).then(json => {
json.listsslcertsresponse.sslcert.forEach(entry => this.sslcerts.data.push(entry))
if (json.listsslcertsresponse.sslcert && json.listsslcertsresponse.sslcert.length > 0 && this.selectedSsl.id == null) {
this.selectedSsl.name = json.listsslcertsresponse.sslcert[0].name
this.selectedSsl.id = json.listsslcertsresponse.sslcert[0].id
}
}).catch(error => {
this.$notifyError(error)
})
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.sslcerts.loading = false
})
if (this.selectedRule !== null) {
this.getCurrentAssignedSslCert()
}
},
getCurrentAssignedSslCert () {
getAPI('listSslCerts', {
accountid: this.currentAccountId,
lbruleid: this.selectedRule.id
}).then(json => {
if (json.listsslcertsresponse.sslcert && json.listsslcertsresponse.sslcert.length > 0) {
this.assignedSslCert = json.listsslcertsresponse.sslcert[0].name
this.deleteSslButtonVisible = true
} else {
this.assignedSslCert = 'None'
this.deleteSslButtonVisible = false
}
}).catch(error => {
this.$notifyError(error)
})
},
selectssl (e) {
this.selectedSsl.id = e
const sslcert = this.sslcerts.data.find(entry => entry.id === this.selectedSsl.id)
if (sslcert) {
this.selectedSsl.name = sslcert.name
}
},
handleAddSslCert (data) {
this.addSslCert(data, this.selectedSsl.id)
},
addSslTolbRule () {
this.visible = false
this.addSslCert(this.selectedRule.id, this.selectedSsl.id)
},
addSslCert (lbRuleId, certId) {
this.disableSslAddDeleteButtons()
getAPI('assignCertToLoadBalancer', {
lbruleid: lbRuleId,
certid: certId,
forced: true
}).then(response => {
this.$pollJob({
jobId: response.assigncerttoloadbalancerresponse.jobid,
successMessage: this.$t('message.success.assign.sslcert'),
successMethod: () => {
if (this.selectedRule !== null) {
this.getCurrentAssignedSslCert()
}
this.enableSslAddDeleteButtons()
},
errorMessage: this.$t('message.assign.sslcert.failed'),
errorMethod: () => {
},
loadingMessage: this.$t('message.assign.sslcert.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: (e) => {
this.closeModal()
}
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
})
},
removeSslFromLbRule () {
this.disableSslAddDeleteButtons()
getAPI('removeCertFromLoadBalancer', {
lbruleid: this.selectedRule.id
}).then(response => {
this.$pollJob({
jobId: response.removecertfromloadbalancerresponse.jobid,
successMessage: this.$t('message.success.remove.sslcert'),
successMethod: () => {
this.visible = true
this.getCurrentAssignedSslCert()
this.enableSslAddDeleteButtons()
},
errorMessage: this.$t('message.remove.sslcert.failed'),
errorMethod: () => {
this.visible = true
},
loadingMessage: this.$t('message.remove.sslcert.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.closeModal()
}
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
})
},
enableSslAddDeleteButtons () {
this.deleteSslButtonVisible = true
this.addSslButtonVisible = true
},
disableSslAddDeleteButtons () {
this.addSslButtonVisible = false
this.deleteSslButtonVisible = false
},
handleOpenAddSslCertModal (record) {
this.addSslCertModalVisible = true
if (record) {
this.showAssignedSsl = true
this.addSslButtonVisible = true
this.selectedSsl = {}
} else {
this.showAssignedSsl = false
this.addSslButtonVisible = false
}
this.fetchSslCerts()
},
returnAlgorithmName (name) { returnAlgorithmName (name) {
switch (name) { switch (name) {
case 'leastconn': case 'leastconn':
@ -1705,6 +1932,9 @@ export default {
successMessage: this.$t('message.success.assign.vm'), successMessage: this.$t('message.success.assign.vm'),
successMethod: () => { successMethod: () => {
this.parentToggleLoading() this.parentToggleLoading()
if (this.newRule.protocol === 'ssl' && this.selectedSsl.id !== null) {
this.handleAddSslCert(data)
}
this.fetchData() this.fetchData()
this.closeModal() this.closeModal()
}, },
@ -1780,6 +2010,7 @@ export default {
this.addNetworkModalLoading = false this.addNetworkModalLoading = false
this.addNetworkModalVisible = false this.addNetworkModalVisible = false
this.selectedTierForAutoScaling = null this.selectedTierForAutoScaling = null
this.addSslCertModalVisible = null
}, },
handleChangePage (page, pageSize) { handleChangePage (page, pageSize) {
this.page = page this.page = page