Add centos-9-stream job

Add octavia-v2-dsvm-scenario-centos-9-stream

Add extra args to the "scp" command to authorize the pubkey exchange
with a cirros VM.
Detect the openssh client version to enable the use of the SCP protocol
(starting with openssh 8.7, the SFTP protocol is the default protocol
with scp, SFTP is not supported by Cirros) when sending files to the
cirros VM.

Depends-On: https://review.opendev.org/828189

Change-Id: I689a50e6762fd22e1aaec8aa84ff5b075ff5bd45
diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py
index e7a344d..d22d81d 100644
--- a/octavia_tempest_plugin/tests/test_base.py
+++ b/octavia_tempest_plugin/tests/test_base.py
@@ -15,6 +15,7 @@
 import ipaddress
 import os
 import random
+import re
 import shlex
 import string
 import subprocess
@@ -976,6 +977,31 @@
         return webserver_details
 
     @classmethod
+    def _get_openssh_version(cls):
+        p = subprocess.Popen(["ssh", "-V"],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        output = p.communicate()[1]
+
+        try:
+            m = re.match(r"OpenSSH_(\d+)\.(\d+)", output.decode('utf-8'))
+            version_maj = int(m.group(1))
+            version_min = int(m.group(2))
+            return version_maj, version_min
+        except Exception:
+            return None, None
+
+    @classmethod
+    def _need_scp_protocol(cls):
+        # When using scp >= 8.7, force the use of the SCP protocol,
+        # the new default (SFTP protocol) doesn't work with
+        # cirros VMs.
+        ssh_version = cls._get_openssh_version()
+        LOG.debug("ssh_version = {}".format(ssh_version))
+        return (ssh_version[0] > 8 or
+                (ssh_version[0] == 8 and ssh_version[1] >= 7))
+
+    @classmethod
     def _install_start_webserver(cls, ip_address, ssh_key, start_id,
                                  revoke_cert=False):
         local_file = CONF.load_balancer.test_server_path
@@ -987,14 +1013,20 @@
         with tempfile.NamedTemporaryFile() as key:
             key.write(ssh_key.encode('utf-8'))
             key.flush()
+            ssh_extra_args = (
+                "-o PubkeyAcceptedKeyTypes=+ssh-rsa")
+            if cls._need_scp_protocol():
+                ssh_extra_args += " -O"
             cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
+                   "{7} "
                    "-o StrictHostKeyChecking=no "
                    "-o ConnectTimeout={0} -o ConnectionAttempts={1} "
                    "-i {2} {3} {4}@{5}:{6}").format(
                 CONF.load_balancer.scp_connection_timeout,
                 CONF.load_balancer.scp_connection_attempts,
                 key.name, local_file, CONF.validation.image_ssh_user,
-                ip_address, const.TEST_SERVER_BINARY)
+                ip_address, const.TEST_SERVER_BINARY,
+                ssh_extra_args)
             args = shlex.split(cmd)
             subprocess_args = {'stdout': subprocess.PIPE,
                                'stderr': subprocess.STDOUT,
@@ -1151,14 +1183,20 @@
             subprocess_args = {'stdout': subprocess.PIPE,
                                'stderr': subprocess.STDOUT,
                                'cwd': None}
+            ssh_extra_args = (
+                "-o PubkeyAcceptedKeyTypes=+ssh-rsa")
+            if cls._need_scp_protocol():
+                ssh_extra_args += " -O"
             cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
+                   "{9} "
                    "-o StrictHostKeyChecking=no "
                    "-o ConnectTimeout={0} -o ConnectionAttempts={1} "
                    "-i {2} {3} {4} {5} {6}@{7}:{8}").format(
                 CONF.load_balancer.scp_connection_timeout,
                 CONF.load_balancer.scp_connection_attempts,
                 ssh_key.name, cert_filename, key_filename, client_ca_filename,
-                CONF.validation.image_ssh_user, ip_address, const.DEV_SHM_PATH)
+                CONF.validation.image_ssh_user, ip_address, const.DEV_SHM_PATH,
+                ssh_extra_args)
             args = shlex.split(cmd)
             proc = subprocess.Popen(args, **subprocess_args)
             stdout, stderr = proc.communicate()
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 4b836ea..98bbd88 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -49,6 +49,16 @@
           - controller
 
 - nodeset:
+    name: octavia-single-node-centos-9-stream
+    nodes:
+      - name: controller
+        label: nested-virt-centos-9-stream
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
     name: octavia-two-node
     nodes:
       - name: controller
@@ -707,6 +717,16 @@
         OCTAVIA_AMP_IMAGE_SIZE: 3
 
 - job:
+    name: octavia-v2-dsvm-scenario-centos-9-stream
+    parent: octavia-v2-dsvm-scenario
+    nodeset: octavia-single-node-centos-9-stream
+    vars:
+      devstack_localrc:
+        OCTAVIA_AMP_BASE_OS: centos
+        OCTAVIA_AMP_DISTRIBUTION_RELEASE_ID: 9-stream
+        OCTAVIA_AMP_IMAGE_SIZE: 3
+
+- job:
     name: octavia-v2-dsvm-scenario-ubuntu-focal
     parent: octavia-v2-dsvm-scenario
     vars:
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index 9de7fdc..49ba09d 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -28,6 +28,8 @@
             voting: false
         - octavia-v2-dsvm-scenario-centos-8-stream:
             voting: false
+        - octavia-v2-dsvm-scenario-centos-9-stream:
+            voting: false
         - octavia-v2-act-stdby-dsvm-scenario-two-node:
             voting: false
         - octavia-v2-act-stdby-dsvm-scenario: