DaanHoogland commented on code in PR #11468:
URL: https://github.com/apache/cloudstack/pull/11468#discussion_r2290534093


##########
core/src/main/java/com/cloud/network/HAProxyConfigurator.java:
##########
@@ -469,46 +477,62 @@ private String getLbSubRuleForStickiness(final 
LoadBalancerTO lbTO) {
         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");

Review Comment:
   Can you explain this, @weizhouapache ? is `alpn h2` enough to do the check? 
And is the backend stretch then http 1.1? or is it just the default for the 
frontend? (maybe a link to docs?)



##########
core/src/main/java/com/cloud/network/HAProxyConfigurator.java:
##########
@@ -696,4 +728,26 @@ public String[][] generateFwRules(final 
LoadBalancerConfigCommand lbCmd) {
 
         return result;
     }
+
+    @Override
+    public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand 
lbCmd) {
+        final Set<SslCertEntry> sslCertEntries = new HashSet<SslCertEntry>();
+        for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
+            if (lbTO.getSslCert() != null) {
+                final LbSslCert cert = lbTO.getSslCert();
+                if (cert.isRevoked()) {
+                    continue;
+                }
+                if (lbTO.getLbProtocol() == null || ! 
lbTO.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
+                    continue;
+                }
+                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);

Review Comment:
   ```suggestion
                   addCertEntry(sslCertEntries, lbTO);
   ```
   
   and then 
   ```
       private void addSslCertEntry(Collection<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 = lbTO.getSrcIp().replace(".", "_”) + ’-‘ + 
lbTO.getSrcPort());
           sslCertEntries.add(new SslCertEntry(name, cert.getCert(), 
cert.getKey(), cert.getChain(), cert.getPassword()));
       }
   ```
   sorry , I know I’m a nag and a purist….



##########
server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java:
##########
@@ -1294,8 +1294,13 @@ public boolean assignCertToLoadBalancer(long lbRuleId, 
Long certId) {
 
         //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");
+        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);
+        }

Review Comment:
   new method; validatenewMapRule(..)?



##########
server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java:
##########
@@ -929,6 +929,11 @@ public boolean validateHAProxyLBRule(final 
LoadBalancingRule rule) {
             return false;
         }
 
+        List<String> lbProtocols = Arrays.asList("tcp", "udp", "tcp-proxy", 
"ssl");
+        if (rule.getLbProtocol() != null && 
!lbProtocols.contains(rule.getLbProtocol())) {
+            throw new InvalidParameterValueException("protocol " + 
rule.getLbProtocol() + " is not in valid protocols " + lbProtocols);
+        }

Review Comment:
   new validation method..



##########
core/src/main/java/com/cloud/network/HAProxyConfigurator.java:
##########
@@ -520,61 +544,69 @@ private List<String> getRulesForPool(final LoadBalancerTO 
lbTO, final boolean ke
             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;
                 }
                 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;
     }
 
     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");

Review Comment:
   ```suggestion
               .append(lbCmd.lbStatsUri)
               .append("\n\tstats realm   Haproxy\\ Statistics\n\tstats auth    
")
               .append(lbCmd.lbStatsAuth)
               .append("\n");
   ```



##########
server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java:
##########
@@ -423,19 +432,47 @@ private void validateChain(final List<Certificate> chain, 
final Certificate cert
 
     }
 
-    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) {
+            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);
+        } else if (privateKeyObj instanceof PEMEncryptedKeyPair) {
+            if (password == null) {
+                throw new CloudRuntimeException("Key is encrypted but password 
is null");
+            }
+            PEMEncryptedKeyPair encryptedKeyPair = 
(PEMEncryptedKeyPair)privateKeyObj;
+            privateKey = new JcaPEMKeyConverter().getKeyPair(
+                    encryptedKeyPair.decryptKeyPair(new 
JcePEMDecryptorProviderBuilder().build(password.toCharArray()))).getPrivate();

Review Comment:
   createPEMEncryptedKey(..)



##########
server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java:
##########
@@ -423,19 +432,47 @@ private void validateChain(final List<Certificate> chain, 
final Certificate cert
 
     }
 
-    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) {
+            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);
+        } else if (privateKeyObj instanceof PEMEncryptedKeyPair) {
+            if (password == null) {
+                throw new CloudRuntimeException("Key is encrypted but password 
is null");
+            }
+            PEMEncryptedKeyPair encryptedKeyPair = 
(PEMEncryptedKeyPair)privateKeyObj;
+            privateKey = new JcaPEMKeyConverter().getKeyPair(
+                    encryptedKeyPair.decryptKeyPair(new 
JcePEMDecryptorProviderBuilder().build(password.toCharArray()))).getPrivate();
+        } 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());
         }

Review Comment:
   once we are ready for java21 we should convert to switch on type of 
`privateKeyObj`



##########
test/integration/smoke/test_ssl_offloading.py:
##########
@@ -0,0 +1,553 @@
+# 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
+
+
+_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
+}
+
+USER_DATA="""I2Nsb3VkLWNvbmZpZwoKcnVuY21kOgogIC0gc3VkbyBhcHQtZ2V0IHVwZGF0Z
+QogIC0gc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgYXBhY2hlMgogIC0gc3Vkby
+BzeXN0ZW1jdGwgZW5hYmxlIGFwYWNoZTIKICAtIHN1ZG8gc3lzdGVtY3RsIHN0
+YXJ0IGFwYWNoZTIKICAtIGVjaG8gIlRlc3QgcGFnZSIgfHN1ZG8gdGVlIC92YX
+Ivd3d3L2h0bWwvaW5kZXguaHRtbAoKCg=="""
+
+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
+
+        #Create an account, network, VM and IP addresses
+        cls.account = Account.create(
+                            cls.apiclient,
+                            cls.services["account"],
+                            admin=True,
+                            domainid=cls.domain.id
+                            )
+        cls.user = cls.account.user[0]
+        cls.userapiclient = cls.testClient.getUserApiClient(cls.user.username, 
cls.domain.name)
+
+        # 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
+                                        )
+
+        # 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)
+        cls._cleanup.append(cls.service_offering)
+        cls._cleanup.append(cls.account)

Review Comment:
   should be right after creation, to deal with the case something goes wrong 
during initialisation.



##########
server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java:
##########
@@ -423,19 +432,47 @@ private void validateChain(final List<Certificate> chain, 
final Certificate cert
 
     }
 
-    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) {
+            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);

Review Comment:
   method createPKCS8Key(..)



##########
test/integration/smoke/test_ssl_offloading.py:
##########
@@ -0,0 +1,553 @@
+# 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
+
+
+_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
+}
+
+USER_DATA="""I2Nsb3VkLWNvbmZpZwoKcnVuY21kOgogIC0gc3VkbyBhcHQtZ2V0IHVwZGF0Z
+QogIC0gc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgYXBhY2hlMgogIC0gc3Vkby
+BzeXN0ZW1jdGwgZW5hYmxlIGFwYWNoZTIKICAtIHN1ZG8gc3lzdGVtY3RsIHN0
+YXJ0IGFwYWNoZTIKICAtIGVjaG8gIlRlc3QgcGFnZSIgfHN1ZG8gdGVlIC92YX
+Ivd3d3L2h0bWwvaW5kZXguaHRtbAoKCg=="""
+
+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
+
+        #Create an account, network, VM and IP addresses
+        cls.account = Account.create(
+                            cls.apiclient,
+                            cls.services["account"],
+                            admin=True,
+                            domainid=cls.domain.id
+                            )
+        cls.user = cls.account.user[0]
+        cls.userapiclient = cls.testClient.getUserApiClient(cls.user.username, 
cls.domain.name)
+
+        # 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
+                                        )
+
+        # 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)
+        cls._cleanup.append(cls.service_offering)
+        cls._cleanup.append(cls.account)
+
+    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()
+        if os.path.exists(FULL_CHAIN):
+            os.remove(FULL_CHAIN)
+
+    def wait_for_service_ready(self, command, expected, retries=60):
+        output = None
+        def check_output():
+            try:
+                output = subprocess.check_output(command, 
shell=True).strip().decode('utf-8')
+            except Exception as e:
+                print("Failed to get output of command %s: %s" % (command, e))
+                if expected is None:
+                    print("But it is expected")
+                    return True, None
+                return False, None
+            print("Output of command %s: \n %s" %(command, output))
+            if expected is None:
+                print("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))
+        return res
+
+    @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 -L --connect-timeout 3 http://%s/"; % 
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/"; % (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/"; % (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/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, "SSL certificate problem", 1)
+
+        command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s 
https://%s/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, CONTENT, 1)
+
+        command = "curl -L --connect-timeout 3 --cacert %s --resolve %s:443:%s 
https://%s/"; % (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 -L --connect-timeout 3 http://%s/"; % 
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/"; % (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/"; % (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/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, "SSL certificate problem", 1)
+
+        command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s 
https://%s/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, CONTENT, 1)
+
+        command = "curl -L --connect-timeout 3 --cacert %s --resolve %s:443:%s 
https://%s/"; % (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)

Review Comment:
   same question as on the previous test..



##########
test/integration/smoke/test_ssl_offloading.py:
##########
@@ -0,0 +1,553 @@
+# 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
+
+
+_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
+}
+
+USER_DATA="""I2Nsb3VkLWNvbmZpZwoKcnVuY21kOgogIC0gc3VkbyBhcHQtZ2V0IHVwZGF0Z
+QogIC0gc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgYXBhY2hlMgogIC0gc3Vkby
+BzeXN0ZW1jdGwgZW5hYmxlIGFwYWNoZTIKICAtIHN1ZG8gc3lzdGVtY3RsIHN0
+YXJ0IGFwYWNoZTIKICAtIGVjaG8gIlRlc3QgcGFnZSIgfHN1ZG8gdGVlIC92YX
+Ivd3d3L2h0bWwvaW5kZXguaHRtbAoKCg=="""
+
+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
+
+        #Create an account, network, VM and IP addresses
+        cls.account = Account.create(
+                            cls.apiclient,
+                            cls.services["account"],
+                            admin=True,
+                            domainid=cls.domain.id
+                            )
+        cls.user = cls.account.user[0]
+        cls.userapiclient = cls.testClient.getUserApiClient(cls.user.username, 
cls.domain.name)
+
+        # 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
+                                        )
+
+        # 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)
+        cls._cleanup.append(cls.service_offering)
+        cls._cleanup.append(cls.account)
+
+    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()
+        if os.path.exists(FULL_CHAIN):
+            os.remove(FULL_CHAIN)
+
+    def wait_for_service_ready(self, command, expected, retries=60):
+        output = None
+        def check_output():
+            try:
+                output = subprocess.check_output(command, 
shell=True).strip().decode('utf-8')
+            except Exception as e:
+                print("Failed to get output of command %s: %s" % (command, e))
+                if expected is None:
+                    print("But it is expected")
+                    return True, None
+                return False, None
+            print("Output of command %s: \n %s" %(command, output))
+            if expected is None:
+                print("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))
+        return res
+
+    @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 -L --connect-timeout 3 http://%s/"; % 
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/"; % (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/"; % (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/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, "SSL certificate problem", 1)
+
+        command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s 
https://%s/"; % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+        self.wait_for_service_ready(command, CONTENT, 1)
+
+        command = "curl -L --connect-timeout 3 --cacert %s --resolve %s:443:%s 
https://%s/"; % (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)

Review Comment:
   if the delete fails or an exception occurs before here, will the cert be 
deleted with the network?
   
   and how about the lb-rules?



##########
server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java:
##########
@@ -1737,11 +1737,19 @@ private void updateWithLbRules(final DomainRouterJoinVO 
routerJoinVO, final Stri
                         
.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());
+                    if (loadBalancerVO.getLbProtocol() != null && 
loadBalancerVO.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
+                        final LbSslCert sslCert = 
_lbMgr.getLbSslCert(firewallRuleVO.getId());
+                        if (sslCert != null && ! sslCert.isRevoked()) {
+                            
loadBalancingData.append(",sslcert=").append(sourceIp.replace(".", 
"_")).append('-')
+                                    
.append(loadBalancerVO.getSourcePortStart()).append(".pem");
+                        }
+                    }

Review Comment:
   `conditionallyAddLbProtocol(LoadBalancerVO loadBalancerVO, StringBuilder 
loadBalancingData)`..



##########
server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java:
##########
@@ -2257,12 +2262,21 @@ public LoadBalancer 
updateLoadBalancerRule(UpdateLoadBalancerRuleCmd cmd) {
         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)) {
+                    LoadBalancerCertMapVO loadBalancerCertMapVO = 
_lbCertMapDao.findByLbRuleId(lbRuleId);
+                    if (loadBalancerCertMapVO != null) {
+                        logger.debug("Removing SSL cert for load balancer %s 
as the new protocol is not ssl but %s", lbRuleId, lb.getLbProtocol());
+                        _lbCertMapDao.remove(loadBalancerCertMapVO.getId());
+                    }
+                }

Review Comment:
   new method ..



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscr...@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to