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: >
(?x)
^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/resources/certs/dsa_self_signed\.key$|
^server/src/test/resources/certs/non_root\.key$|
@ -57,7 +58,8 @@ repos:
^server/src/test/resources/certs/rsa_self_signed\.key$|
^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$|
^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
exclude: \.vhd$
- id: fix-byte-order-marker
@ -75,7 +77,7 @@ repos:
name: run codespell
description: Check spelling with codespell
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
rev: 7.0.0
hooks:

View File

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

View File

@ -106,7 +106,7 @@ public interface LoadBalancingRulesService {
boolean applyLoadBalancerConfig(long lbRuleId) throws ResourceUnavailableException;
boolean assignCertToLoadBalancer(long lbRuleId, Long certId);
boolean assignCertToLoadBalancer(long lbRuleId, Long certId, boolean isForced);
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.SslCertResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.commons.lang3.BooleanUtils;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
@ -57,11 +58,17 @@ public class AssignCertToLoadBalancerCmd extends BaseAsyncCmd {
description = "the ID of the certificate")
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
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
ResourceAllocationException, NetworkRuleConflictException {
//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());
this.setResponseObject(response);
} else {
@ -95,4 +102,19 @@ public class AssignCertToLoadBalancerCmd extends BaseAsyncCmd {
public Long getLbRuleId() {
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.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;
import com.cloud.dc.DataCenter;
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)")
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;
@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() {
return lbProtocol;
return StringUtils.trim(StringUtils.lowerCase(lbProtocol));
}
/////////////////////////////////////////////////////

View File

@ -82,4 +82,15 @@ public class RemoveCertFromLoadBalancerCmd extends BaseAsyncCmd {
public Long getLbRuleId() {
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 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>();
rules.add(loadBalancerRule);

View File

@ -25,6 +25,7 @@ public class LoadBalancerRule {
private String[] configuration;
private String tmpCfgFilePath;
private String tmpCfgFileName;
private SslCertEntry[] sslCerts;
private String[] addRules;
private String[] removeRules;
@ -32,6 +33,53 @@ public class LoadBalancerRule {
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() {
// Empty constructor for (de)serialization
}
@ -101,4 +149,12 @@ public class LoadBalancerRule {
public void setRouterIp(final String 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.StickinessPolicyTO;
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.utils.Pair;
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 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
public String[] generateConfiguration(final List<PortForwardingRuleTO> fwRules) {
// Group the rules by publicip:publicport
@ -469,30 +477,41 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
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();
final String poolName = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString();
final String publicIP = lbTO.getSrcIp();
final int publicPort = lbTO.getSrcPort();
final String algorithm = lbTO.getAlgorithm();
final List<String> result = new ArrayList<String>();
// add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80"
sb = new StringBuilder();
sb.append("listen ").append(poolName);
result.add(sb.toString());
boolean sslOffloading = lbTO.getSslCert() != null && !lbTO.getSslCert().isRevoked()
&& NetUtils.SSL_PROTO.equals(lbTO.getLbProtocol());
final List<String> frontendConfigs = new ArrayList<>();
final List<String> backendConfigs = new ArrayList<>();
final List<String> result = new ArrayList<>();
sb = new StringBuilder();
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.append("\t").append("balance ").append(algorithm.toLowerCase());
result.add(sb.toString());
backendConfigs.add(sb.toString());
int i = 0;
Boolean destsAvailable = false;
boolean destsAvailable = false;
final String stickinessSubRule = getLbSubRuleForStickiness(lbTO);
final List<String> dstSubRule = new ArrayList<String>();
final List<String> dstWithCookieSubRule = new ArrayList<String>();
final List<String> dstSubRule = new ArrayList<>();
final List<String> dstWithCookieSubRule = new ArrayList<>();
for (final DestinationTO dest : lbTO.getDestinations()) {
// add line like this: "server 65_37_141_30-80_3 10.1.1.4:80 check"
if (dest.isRevoked()) {
@ -500,15 +519,20 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
}
sb = new StringBuilder();
sb.append("\t")
.append("server ")
.append(poolName)
.append("_")
.append(Integer.toString(i++))
.append(" ")
.append(dest.getDestIp())
.append(":")
.append(dest.getDestPort())
.append(" check");
.append("server ")
.append(poolName)
.append("_")
.append(i++)
.append(" ")
.append(dest.getDestIp())
.append(":")
.append(dest.getDestPort())
.append(" check");
if (sslOffloading) {
sb.append(SSL_CONFIGURATION_INTERMEDIATE);
}
if(lbTO.getLbProtocol() != null && lbTO.getLbProtocol().equals("tcp-proxy")) {
sb.append(" send-proxy");
}
@ -520,9 +544,9 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
destsAvailable = true;
}
Boolean httpbasedStickiness = false;
boolean httpbasedStickiness = false;
/* 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()) {
if (stickinessPolicy == null) {
continue;
@ -530,35 +554,40 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName()) ||
StickinessMethodType.AppCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName())) {
httpbasedStickiness = true;
break;
}
}
if (httpbasedStickiness) {
result.addAll(dstWithCookieSubRule);
backendConfigs.addAll(dstWithCookieSubRule);
} else {
result.addAll(dstSubRule);
backendConfigs.addAll(dstSubRule);
}
result.add(stickinessSubRule);
backendConfigs.add(stickinessSubRule);
} else {
result.addAll(dstSubRule);
backendConfigs.addAll(dstSubRule);
}
if (stickinessSubRule != null && !destsAvailable) {
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) {
sb = new StringBuilder();
sb.append("\t").append("mode http");
result.add(sb.toString());
sb = new StringBuilder();
sb.append("\t").append("option httpclose");
result.add(sb.toString());
boolean keepAliveEnabled = lbCmd.keepAliveEnabled;
boolean http = (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled);
if (http || httpbasedStickiness || sslOffloading) {
frontendConfigs.add("\tmode http");
String keepAliveLine = keepAliveEnabled ? "\tno option forceclose" : "\toption httpclose";
frontendConfigs.add(keepAliveLine);
}
// 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();
if (StringUtils.isNotBlank(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);
return result;
}
@ -566,15 +595,18 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
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);
// TODO DH: write test for this in both cases
if (!lbCmd.keepAliveEnabled) {
logger.info("Haproxy mode http enabled");
rule.append("\n\tmode http\n\toption httpclose");
rule.append("\n\tmode http");
if (lbCmd.keepAliveEnabled) {
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 ")
.append(lbCmd.lbStatsUri)
.append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ")
.append(lbCmd.lbStatsAuth);
rule.append("\n");
.append(lbCmd.lbStatsUri)
.append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ")
.append(lbCmd.lbStatsAuth)
.append("\n");
final String result = rule.toString();
if (logger.isDebugEnabled()) {
logger.debug("Haproxystats rule: " + result);
@ -644,7 +676,7 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
if (lbTO.isRevoked()) {
continue;
}
final List<String> poolRules = getRulesForPool(lbTO, lbCmd.keepAliveEnabled);
final List<String> poolRules = getRulesForPool(lbTO, lbCmd);
result.addAll(poolRules);
has_listener = true;
}
@ -696,4 +728,30 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
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.to.PortForwardingRuleTO;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
public interface LoadBalancerConfigurator {
public final static int ADD = 0;
@ -34,4 +35,6 @@ public interface LoadBalancerConfigurator {
public String[] generateConfiguration(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.LoadBalancerRules;
import com.cloud.network.lb.LoadBalancingRule.LbDestination;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.Networks.TrafficType;
public class ConfigHelperTest {
@ -223,9 +224,12 @@ public class ConfigHelperTest {
protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() {
final List<LoadBalancerTO> lbs = 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", 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()];
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.to.LoadBalancerTO;
import com.cloud.network.lb.LoadBalancingRule.LbDestination;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import java.util.List;
import java.util.ArrayList;
@ -80,11 +81,11 @@ public class HAProxyConfiguratorTest {
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);
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);
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
// create lb command
// 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"));
}
@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) {
String[] sa = hpg.generateConfiguration(cmd);
StringBuilder sb = new StringBuilder();

View File

@ -22,4 +22,6 @@ import com.cloud.utils.db.GenericDao;
public interface SslCertDao extends GenericDao<SslCertVO, Long> {
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);
}
@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>();
lbCapabilities.put(Capability.SupportedLBAlgorithms, "roundrobin,leastconn,source");
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.LbSchemes, LoadBalancerContainer.Scheme.Public.toString());
// Supports SSL offloading
lbCapabilities.put(Capability.SslTermination, "true");
// specifies that LB rules can support autoscaling and the list of
// counters it supports

View File

@ -1267,10 +1267,10 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
@Override
@DB
@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();
LoadBalancerVO loadBalancer = _lbDao.findById(Long.valueOf(lbRuleId));
LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
if (loadBalancer == null) {
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");
}
//check if the lb is already bound
LoadBalancerCertMapVO certMapRule = _lbCertMapDao.findByLbRuleId(loadBalancer.getId());
if (certMapRule != null)
throw new InvalidParameterValueException("Another certificate is already bound to the LB");
validateCertMapRule(lbRuleId, forced);
//check for correct port
if (loadBalancer.getLbProtocol() == null || !(loadBalancer.getLbProtocol().equals(NetUtils.SSL_PROTO)))
@ -1326,6 +1323,18 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
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
@DB
@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;
}
private LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) {
protected LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) {
List<LbStickinessPolicy> policyList = getStickinessPolicies(lb.getId());
Ip sourceIp = getSourceIp(lb);
@ -2257,12 +2266,17 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
LoadBalancerVO tmplbVo = _lbDao.findById(lbRuleId);
boolean success = _lbDao.update(lbRuleId, lb);
// If algorithm is changed, have to reapply the lb config
if ((algorithm != null) && (tmplbVo.getAlgorithm().compareTo(algorithm) != 0)){
// If algorithm or lb protocol is changed, have to reapply the lb config
boolean needToReApplyRule = (algorithm != null && !algorithm.equals(tmplbVo.getAlgorithm()))
|| (lbProtocol != null && !lbProtocol.equals(tmplbVo.getLbProtocol()));
if (needToReApplyRule) {
try {
lb.setState(FirewallRule.State.Add);
_lbDao.persist(lb);
applyLoadBalancerConfig(lbRuleId);
if (!lb.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
removeCertMapIfExists(lb);
}
} catch (ResourceUnavailableException e) {
if (isRollBackAllowedForProvider(lb)) {
/*
@ -2279,6 +2293,9 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
if (lbBackup.getAlgorithm() != null) {
lb.setAlgorithm(lbBackup.getAlgorithm());
}
if (lbBackup.getLbProtocol() != null) {
lb.setLbProtocol(lbBackup.getLbProtocol());
}
lb.setState(lbBackup.getState());
_lbDao.update(lb.getId(), 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
public Pair<List<? extends UserVm>, List<String>> listLoadBalancerInstances(ListLoadBalancerRuleInstancesCmd cmd) throws PermissionDeniedException {
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);
lb.setCidrList(rule.getCidrList());
lb.setLbProtocol(lb_protocol);
lb.setLbSslCert(rule.getLbSslCert());
lbs[i++] = lb;
}
String routerPublicIp = null;

View File

@ -929,6 +929,8 @@ public class NetworkHelperImpl implements NetworkHelper {
return false;
}
validateHAproxyLbProtocol(rule.getLbProtocol());
for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) {
final List<Pair<String, String>> paramsList = stickinessPolicy.getParams();
@ -982,6 +984,13 @@ public class NetworkHelperImpl implements NetworkHelper {
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
* 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());
if (firewallRuleVO instanceof LoadBalancerVO) {
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(",destPortEnd=").append(loadBalancerVO.getDefaultPortEnd())
.append(",algorithm=").append(loadBalancerVO.getAlgorithm())
.append(",protocol=").append(loadBalancerVO.getLbProtocol());
updateWithLbRuleSslCertificates(loadBalancingData, loadBalancerVO, sourceIp);
} else if (firewallRuleVO instanceof ApplicationLoadBalancerRuleVO) {
ApplicationLoadBalancerRuleVO appLoadBalancerVO = (ApplicationLoadBalancerRuleVO) firewallRuleVO;
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) {
Map<String, String> data = new HashMap<>();
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.RemoteAccessVpnDao;
import com.cloud.network.dao.RemoteAccessVpnVO;
import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.security.SecurityGroupManager;
@ -309,6 +310,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
private UserDataDao userDataDao;
@Inject
private NetworkPermissionDao networkPermissionDao;
@Inject
private SslCertDao sslCertDao;
private List<QuerySelector> _querySelectors;
@ -1203,6 +1206,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
// Delete registered UserData
userDataDao.removeByAccountId(accountId);
// Delete SSL certificates
sslCertDao.removeByAccountId(accountId);
// Delete Webhooks
deleteWebhooksForAccount(accountId);

View File

@ -26,8 +26,9 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore;
@ -48,10 +49,6 @@ import java.util.HashSet;
import java.util.List;
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 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.network.tls.CertService;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
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.PemReader;
@ -89,7 +98,6 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.security.CertificateHelper;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class CertServiceImpl implements CertService {
@ -279,11 +287,11 @@ public class CertServiceImpl implements CertService {
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 {
List<Certificate> chain = null;
final Certificate cert = parseCertificate(certInput);
final PrivateKey key = parsePrivateKey(keyInput);
final PrivateKey key = parsePrivateKey(keyInput, password);
if (chainInput != null) {
chain = CertificateHelper.parseChain(chainInput);
@ -295,7 +303,9 @@ public class CertServiceImpl implements CertService {
if (chainInput != null) {
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);
}
}
@ -370,18 +380,17 @@ public class CertServiceImpl implements CertService {
try {
final String data = "ENCRYPT_DATA";
final SecureRandom random = new SecureRandom();
final Cipher cipher = Cipher.getInstance(pubKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privKey, random);
final byte[] encryptedData = cipher.doFinal(data.getBytes());
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privKey);
sig.update(data.getBytes());
byte[] signature = sig.sign();
cipher.init(Cipher.DECRYPT_MODE, pubKey, random);
final String decreptedData = new String(cipher.doFinal(encryptedData));
if (!decreptedData.equals(data)) {
sig.initVerify(pubKey);
sig.update(data.getBytes());
if (!sig.verify(signature)) {
throw new IllegalStateException("Bad public-private key");
}
} catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException e) {
} catch (final InvalidKeyException | SignatureException e) {
throw new IllegalStateException("Bad public-private key", e);
} catch (final NoSuchAlgorithmException 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));
try (final PemReader pemReader = new PemReader(new StringReader(key));) {
final PemObject pemObject = pemReader.readPemObject();
final byte[] content = pemObject.getContent();
final PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
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);
PEMParser pemParser = new PEMParser(new StringReader(key));
Object privateKeyObj = pemParser.readObject();
if (privateKeyObj == null) {
throw new CloudRuntimeException("Cannot parse private key");
}
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

View File

@ -17,12 +17,30 @@
package com.cloud.network.lb;
import com.cloud.exception.ResourceUnavailableException;
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.NetworkDao;
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.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.command.user.loadbalancer.UpdateLoadBalancerRuleCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.junit.Assert;
import org.junit.Test;
@ -32,11 +50,16 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList;
import java.util.Arrays;
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;
@RunWith(MockitoJUnitRunner.class)
@ -48,10 +71,39 @@ public class LoadBalancingRulesManagerImplTest{
@Mock
NetworkOrchestrationService _networkMgr;
@Mock
LoadBalancerDao _lbDao;
@Mock
EntityManager _entityMgr;
@Mock
AccountManager _accountMgr;
@Mock
NetworkModel _networkModel;
@Mock
LoadBalancerCertMapDao _lbCertMapDao;
@Mock
NetworkOfferingServiceMapDao _networkOfferingServiceDao;
@Spy
@InjectMocks
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
public void generateCidrStringTestNullCidrList() {
String result = lbr.generateCidrString(null);
@ -83,7 +135,7 @@ public class LoadBalancingRulesManagerImplTest{
List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter);
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);
Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock);
@ -101,4 +153,159 @@ public class LoadBalancingRulesManagerImplTest{
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.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.MonitoringServiceDao;
import com.cloud.network.dao.NetworkDao;
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.VirtualRouterProviderDao;
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.vpc.VpcVO;
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.UserStatisticsDao;
import com.cloud.user.dao.UserStatsLogDao;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
@ -259,6 +263,9 @@ public class VirtualNetworkApplianceManagerImplTest {
@Mock
private BGPService bgpService;
@Mock
private LoadBalancingRulesManager _lbMgr;
// @InjectMocks
// private VirtualNetworkApplianceManagerImpl virtualNetworkApplianceManagerImpl;
@ -391,4 +398,21 @@ public class VirtualNetworkApplianceManagerImplTest {
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.remove(Mockito.anyLong())).thenReturn(true);
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).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.NetworkDao;
import com.cloud.network.dao.RemoteAccessVpnDao;
import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.security.SecurityGroupManager;
import com.cloud.network.security.dao.SecurityGroupDao;
@ -198,6 +199,8 @@ public class AccountManagetImplTestBase {
@Mock
UserDataDao userDataDao;
@Mock
SslCertDao sslCertDao;
@Mock
NetworkPermissionDao networkPermissionDaoMock;
@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.UploadSslCertCmd;
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.Assert;
import org.junit.Assume;
@ -44,9 +51,13 @@ import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -207,7 +218,7 @@ public class CertServiceTest {
}
}
// @Test
@Test
/**
* 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.");
} catch (final Exception e) {
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");
} catch (final Exception e) {
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;
}
}
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.
import logging
import os.path
from os import listdir
import re
from cs.CsDatabag import CsDataBag
from .CsProcess import CsProcess
@ -25,6 +26,7 @@ from . import CsHelper
HAPROXY_CONF_T = "/etc/haproxy/haproxy.cfg.new"
HAPROXY_CONF_P = "/etc/haproxy/haproxy.cfg"
SSL_CERTS_DIR = "/etc/cloudstack/ssl/"
class CsLoadBalancer(CsDataBag):
""" Manage Load Balancer entries """
@ -34,6 +36,9 @@ class CsLoadBalancer(CsDataBag):
return
if 'configuration' not in list(self.dbag['config'][0].keys()):
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']
file1 = CsFile(HAPROXY_CONF_T)
file1.empty()
@ -43,6 +48,11 @@ class CsLoadBalancer(CsDataBag):
file1.commit()
file2 = CsFile(HAPROXY_CONF_P)
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)
proc = CsProcess(['/run/haproxy.pid'])
@ -82,3 +92,29 @@ class CsLoadBalancer(CsDataBag):
ip = path[0]
port = path[1]
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:
response = requests.post(url,
params=payload,
data=payload,
cert=self.certPath,
verify=self.httpsFlag)
self.logger.debug("=======Got POST response : %s=======" % response)
return response
except Exception as e:
self.__lastError = e

View File

@ -1068,7 +1068,7 @@ test_data = {
"displaytext": "ubuntu 22.04 kvm",
"format": "raw",
"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",
"requireshvm": "True",
"ispublic": "True",

View File

@ -3080,6 +3080,9 @@ class LoadBalancerRule:
if "openfirewall" in services:
cmd.openfirewall = services["openfirewall"]
if "protocol" in services:
cmd.protocol = services["protocol"]
if projectid:
cmd.projectid = projectid
@ -3188,6 +3191,22 @@ class LoadBalancerRule:
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
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:
"""Manage Cluster life cycle"""
@ -8016,3 +8035,60 @@ class GpuDevice:
cmd.id = self.id
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
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.certchain": "Chain",
"label.certificate": "Certificate",
"label.certificate.chain": "Certificate chain",
"label.certificate.upload": "Certificate uploaded.",
"label.certificate.upload.failed": "Certificate upload failed",
"label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check.",
"label.certificateid": "Certificate ID",
"label.certificates": "Certificates",
"label.chainsize": "Chain size",
"label.change": "Change",
"label.change.affinity": "Change affinity",
@ -972,6 +974,7 @@
"label.enable.vpn": "Enable remote access VPN",
"label.enable.webhook": "Enable Webhook",
"label.enabled": "Enabled",
"label.enabled.revocation.check": "Enables revocation checking for certificates",
"label.encrypt": "Encrypt",
"label.encryptroot": "Encrypt Root Disk",
"label.end": "End",
@ -1480,6 +1483,7 @@
"label.make.user.project.owner": "Make User project owner",
"label.makeredundant": "Make redundant",
"label.manage": "Manage",
"label.manage.ssl.cert": "Manage SSL certificate",
"label.manage.vpn.user": "Manage VPN Users",
"label.managed.instances": "Managed Instances",
"label.managed.volumes": "Managed Volumes",
@ -2053,6 +2057,7 @@
"label.remove.vpc.offering": "Remove VPC Offering",
"label.removed": "Removed",
"label.removing": "Removing",
"label.replace": "Replace",
"label.replace.acl": "Replace ACL",
"label.report.bug": "Ask a question or Report an issue",
"label.request": "Request",
@ -2312,6 +2317,7 @@
"label.uefi.supported": "UEFI supported",
"label.unregister.extension": "Unregister Extension",
"label.usediops": "IOPS used",
"label.userdata": "User Data",
"label.user.data.id": "User Data ID",
"label.user.data.name": "User Data name",
"label.user.data.details": "User Data details",
@ -2327,6 +2333,8 @@
"label.ssh.port": "SSH port",
"label.sshkeypair": "New SSH key pair",
"label.sshkeypairs": "SSH key pairs",
"label.ssl": "SSL",
"label.sslcertificate": "SSL certificate",
"label.sslcertificates": "SSL certificates",
"label.sslverification": "SSL verification",
"label.standard.us.keyboard": "Standard (US) keyboard",
@ -2587,6 +2595,7 @@
"label.upload.icon": "Upload icon",
"label.upload.iso.from.local": "Upload ISO from local",
"label.upload.resource.icon": "Upload icon",
"label.upload.ssl.certificate": "Upload SSL cerficicate",
"label.upload.template.from.local": "Upload Template from local",
"label.upload.volume": "Upload volume",
"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.processing": "Removing 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.nsx.controller": "Add NSX Provider",
"message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>",
@ -3047,6 +3058,8 @@
"message.allowed": "Allowed",
"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.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.vm.failed": "Failed to assign Instance",
"message.assign.vm.processing": "Assigning Instance...",
@ -3762,6 +3775,7 @@
"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.gateway": "Successfully added VPN gateway",
"message.success.assign.sslcert": "Successfully assigned SSL certificate",
"message.success.assign.vm": "Successfully assigned Instance",
"message.success.apply.network.policy": "Successfully applied Network Policy",
"message.success.apply.tungsten.tag": "Successfully applied Tag",
@ -3840,6 +3854,7 @@
"message.success.release.ip": "Successfully released IP",
"message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer",
"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.objectstore.objects": "Successfully removed selected object(s)",
"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.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.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.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?",

View File

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

View File

@ -46,6 +46,10 @@ export default {
'listProjectRoles' in store.getters.apis
}
},
{
name: 'certificates',
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue')))
},
{
name: 'limits',
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">{{ $t('label.tcp') }}</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>
</div>
<div :span="24" class="action-button">

View File

@ -17,6 +17,17 @@
<template>
<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-col :md="24" :lg="24">
<a-table
@ -68,16 +79,120 @@
</a-list>
</a-col>
</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>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
import { ref, reactive, toRaw } from 'vue'
export default {
name: 'SSLCertificate',
components: {
TooltipLabel,
TooltipButton
},
data () {
@ -90,7 +205,9 @@ export default {
page: 1,
pageSize: 10,
quickview: false,
loading: false
loading: false,
uploading: false,
showUploadForm: false
}
},
props: {
@ -127,6 +244,9 @@ export default {
}
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('uploadSslCert')
},
created () {
this.columns = [
{
@ -149,14 +269,28 @@ export default {
}
]
this.detailColumn = ['name', 'certificate', 'certchain']
this.initForm()
this.fetchData()
},
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 () {
const params = {}
params.page = this.page
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
@ -224,6 +358,46 @@ export default {
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 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="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select>
</div>
<div class="form__item">
@ -84,6 +85,12 @@
<a-select-option value="no">{{ $t('label.no') }}</a-select-option>
</a-select>
</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__label" style="white-space: nowrap;">{{ $t('label.add.vms') }}</div>
<a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddVMModal">
@ -139,6 +146,12 @@
{{ returnStickinessLabel(record.id) }}
</a-button>
</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'">
<div>
<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" :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="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select>
</div>
<div :span="24" class="action-button">
@ -552,6 +566,60 @@
</div>
</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
:title="$t('label.select.tier')"
:maskClosable="false"
@ -791,6 +859,20 @@ export default {
totalCount: 0,
page: 1,
pageSize: 10,
sslcerts: {
loading: false,
data: []
},
selectedSsl: {
name: '',
id: null
},
addSslCertModalVisible: false,
showAssignedSsl: false,
currentAccountId: null,
assignedSslCert: 'None',
deleteSslButtonVisible: true,
addSslButtonVisible: true,
columns: [
{
title: this.$t('label.name'),
@ -824,6 +906,10 @@ export default {
key: 'add',
title: this.$t('label.add.vms')
},
{
key: 'sslcert',
title: this.$t('label.sslcertificate')
},
{
key: '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) {
switch (name) {
case 'leastconn':
@ -1705,6 +1932,9 @@ export default {
successMessage: this.$t('message.success.assign.vm'),
successMethod: () => {
this.parentToggleLoading()
if (this.newRule.protocol === 'ssl' && this.selectedSsl.id !== null) {
this.handleAddSslCert(data)
}
this.fetchData()
this.closeModal()
},
@ -1780,6 +2010,7 @@ export default {
this.addNetworkModalLoading = false
this.addNetworkModalVisible = false
this.selectedTierForAutoScaling = null
this.addSslCertModalVisible = null
},
handleChangePage (page, pageSize) {
this.page = page