]> git.apps.os.sepia.ceph.com Git - teuthology.git/commitdiff
Revive teuthology-openstack
authordeepssin <deepssin@redhat.com>
Mon, 16 Jun 2025 13:55:21 +0000 (09:55 -0400)
committerdeepssin <deepssin@redhat.com>
Mon, 16 Jun 2025 13:55:21 +0000 (09:55 -0400)
Signed-off-by: deepssin <deepssin@redhat.com>
bootstrap
requirements.txt
setup.cfg
teuthology/openstack/__init__.py
teuthology/openstack/setup-openstack.sh
teuthology/suite/run.py

index 5f9085e82450aa3701a8c47d8e65328a5a90c76a..6e6bfd052643c8ab6e12dfa8b807ddc50b047fd4 100755 (executable)
--- a/bootstrap
+++ b/bootstrap
@@ -26,6 +26,7 @@ if [ -z "$PYTHON" ]; then
   # This would be bizarre, but I suppose possible
   PYTHON=${PYTHON:-"python3"}
 fi
+echo "Using python: $PYTHON"
 
 case "$(uname -s)" in
 Linux)
@@ -40,8 +41,8 @@ Linux)
     install_pkg="sudo dnf install -y"
     case "$OS" in
     Ubuntu|Debian|LinuxMint)
-        deps=(qemu-utils python3-dev libssl-dev python3-pip python3-wheel $PYTHON-venv libev-dev libvirt-dev libffi-dev libyaml-dev)
-        has_pkg="dpkg -C"
+        deps=(qemu-utils python3-dev libssl-dev python3-pip python3-wheel python3-venv libev-dev libvirt-dev libffi-dev libyaml-dev build-essential jq curl)
+        has_pkg="dpkg -s"
         install_pkg="sudo apt install -y"
         ;;
     RedHatEnterpriseWorkstation|RedHatEnterpriseServer|RedHatEnterprise|CentOS)
@@ -114,17 +115,26 @@ if [ -z "$NO_CLOBBER" ] && \
   [ ! -e "$VENV/bin/pip" -o ! -e "$VENV/bin/$PYTHON" ] || \
   [ "${PYTHON_VER_OUT}" != "$($VENV/bin/$PYTHON --version)" ] \
   ; then
+   echo "Deleting existing virtual environment"
     rm -rf virtualenv
 fi
 
 if [ -z "$NO_CLOBBER" ] || [ ! -e $VENV ]; then
+    echo "Creating new venv at $VENV"
     $PYTHON_BIN -m venv $VENV
 fi
 
+PY_MAJOR=$($VENV/bin/python -c "import sys; print(sys.version_info[0])")
+PY_MINOR=$($VENV/bin/python -c "import sys; print(sys.version_info[1])")
+
+# Downgrade pip for older python versions
+if [ "$PY_MAJOR" -eq 3 ] && [ "$PY_MINOR" -lt 6 ]; then
+    $VENV/bin/python -m ensurepip
+    $VENV/bin/pip install pip==20.3.4 --force-reinstall
+fi
+
 $VENV/bin/pip install packaging
 
-# It is impossible to upgrade ansible from 2.9 to 2.10 via pip.
-# See https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_2.10.html#known-issues
 if [ -f "$VENV/bin/ansible" ]; then
     uninstall_ansible=$($VENV/bin/python3 -c "import ansible; from packaging.version import parse; print(parse(ansible.__version__) < parse('2.10.0'))")
     if [ "$uninstall_ansible" = "True" ]; then
@@ -146,3 +156,5 @@ $VENV/bin/pip check
 
 # Install ansible collections
 $VENV/bin/ansible-galaxy install -r requirements.yml
+
+echo "Bootstrap completed successfully!!!"
index c4a05b2f4d1a8664da5301e5334387db43729171..a5245266c9865d6e06d6cde03f88a3f30a70a4ba 100644 (file)
@@ -200,7 +200,9 @@ zope-event==5.0
     # via gevent
 zope-interface==6.1
     # via gevent
-
+openstacksdk==4.5.0
+    # via teuthology (pyproject.toml)
+python-openstackclient>=6.0.0
 # The following packages are considered to be unsafe in a requirements file:
 # pip
 # setuptools
index a18d92bb7e5b5010079ad7a103417d6ab336d7de..c03da52ad74eba06302099baa35397650186b51c 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -110,6 +110,7 @@ test =
 openstack =
     python-openstackclient
     python-novaclient
+    openstacksdk
 
 [options.package_data]
 teuthology.openstack =
index 0f626e55c9c426729f472d45b6279a00d8771da2..1fb5481917564e55b83f7d4a985183707c6b8b77 100644 (file)
@@ -46,6 +46,7 @@ from teuthology.config import config as teuth_config
 from teuthology.config import set_config_attr
 from teuthology.orchestra import connection
 from teuthology import misc
+from openstack import connection as openstack_connection
 
 from yaml.representer import SafeRepresenter
 
@@ -78,6 +79,8 @@ class OpenStackInstance(object):
         self.name_or_id = name_or_id
         self.private_or_floating_ip = None
         self.private_ip = None
+        self.info = info
+        self.conn = self._create_connection()
         if info is None:
             self.set_info()
         else:
@@ -88,11 +91,14 @@ class OpenStackInstance(object):
                 errmsg = '{}: {}'.format(errmsg, self.info['message'])
             raise Exception(errmsg)
 
+    def _create_connection(self):
+        return openstack_connection.from_config(cloud=None)
+
     def set_info(self):
         try:
-            self.info = json.loads(
-                OpenStack().run("server show -f json " + self.name_or_id))
-            enforce_json_dictionary(self.info)
+            server = self.conn.compute.find_server(self.name_or_id)
+            if server:
+                self.info = {k.lower(): v for k, v in server.to_dict().items()}
         except CalledProcessError:
             self.info = None
 
@@ -129,32 +135,17 @@ class OpenStackInstance(object):
                 self.set_info()
 
     def get_ip_neutron(self):
-        subnets = json.loads(misc.sh("unset OS_AUTH_TYPE OS_TOKEN ; "
-                                     "neutron subnet-list -f json -c id -c ip_version"))
-        subnet_ids = []
-        for subnet in subnets:
-            if subnet['ip_version'] == 4:
-                subnet_ids.append(subnet['id'])
-        if not subnet_ids:
-            raise Exception("no subnet with ip_version == 4")
-        ports = json.loads(misc.sh("unset OS_AUTH_TYPE OS_TOKEN ; "
-                                   "neutron port-list -f json -c fixed_ips -c device_id"))
-        fixed_ips = None
+        conn = OpenStack().conn
+        subnets = [subnet.id for subnet in conn.network.subnets() if subnet.ip_version == 4]
+        if not subnets:
+            raise Exception("No subnet with ip_version == 4 found")
+        ports = conn.network.ports(device_id=self['id'])
         for port in ports:
-            if port['device_id'] == self['id']:
-                fixed_ips = port['fixed_ips'].split("\n")
-                break
-        if not fixed_ips:
-            raise Exception("no fixed ip record found")
-        ip = None
-        for fixed_ip in fixed_ips:
-            record = json.loads(fixed_ip)
-            if record['subnet_id'] in subnet_ids:
-                ip = record['ip_address']
-                break
-        if not ip:
-            raise Exception("no ip")
-        return ip
+            for fixed_ip in port.fixed_ips:
+                if fixed_ip.get('subnet_id') in subnets:
+                    return fixed_ip['ip_address']
+
+        raise Exception("No IP found for instance")
 
     def get_ip(self, network):
         """
@@ -221,6 +212,14 @@ class OpenStack(object):
         'ubuntu-16.04-x86_64': 'https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img',
         'ubuntu-16.04-aarch64': 'https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-arm64-disk1.img',
         'ubuntu-16.04-i686': 'https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-i386-disk1.img',
+        'ubuntu-18.04-x86_64': 'https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img',
+        'ubuntu-18.04-aarch64': 'https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-arm64.img',
+        'ubuntu-18.04-i686': 'https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-i386.img',
+        'ubuntu-20.04-x86_64': 'https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img',
+        'ubuntu-20.04-aarch64': 'https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img',
+        'ubuntu-22.04-x86_64': 'https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img',
+        'ubuntu-22.04-aarch64': 'https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img',
+        'ubuntu-24.04-x86_64': 'https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img',
         'debian-8.0-x86_64': 'http://cdimage.debian.org/cdimage/openstack/current/debian-8.7.1-20170215-openstack-amd64.qcow2',
     }
 
@@ -230,11 +229,15 @@ class OpenStack(object):
         self.username = 'ubuntu'
         self.up_string = "UNKNOWN"
         self.teuthology_suite = 'teuthology-suite'
+        self.conn = self._create_connection()
 
     token = None
     token_expires = None
     token_cache_duration = 3600
 
+    def _create_connection(self):
+        return openstack_connection.from_config(cloud=None)
+
     def cache_token(self):
         if self.provider != 'ovh':
             return False
@@ -348,9 +351,10 @@ class OpenStack(object):
         """
         Return the uuid of the network in OpenStack.
         """
-        r = json.loads(self.run("network show -f json " +
-                               network))
-        return self.get_value(r, 'id')
+        conn = self.conn
+        network = conn.network.find_network(network)
+        if network:
+            return network.id
 
     def type_version_arch(self, os_type, os_version, arch):
         """
@@ -409,11 +413,10 @@ class OpenStack(object):
 
     @staticmethod
     def sort_flavors(flavors):
-        def sort_flavor(a, b):
-            return (a['VCPUs'] - b['VCPUs'] or
-                    a['RAM'] - b['RAM'] or
-                    a['Disk'] - b['Disk'])
-        return sorted(flavors, cmp=sort_flavor)
+        def sort_key(flavor):
+            # Create a tuple for sorting: (VCPUs, RAM, Disk)
+            return (flavor['VCPUs'], flavor['RAM'], flavor['Disk'])
+        return sorted(flavors, key=sort_key)
 
     def get_os_flavors(self):
         flavors = json.loads(self.run("flavor list -f json"))
@@ -533,19 +536,21 @@ class OpenStack(object):
 
     @staticmethod
     def list_instances():
+        conn = OpenStack().conn
         ownedby = "ownedby='" + teuth_config.openstack['ip'] + "'"
-        all = json.loads(OpenStack().run(
-            "server list -f json --long --name 'target'"))
-        return filter(lambda instance: ownedby in instance['Properties'], all)
+        instances = conn.compute.servers(all_projects=True)
+        return [inst for inst in instances if ownedby in (getattr(inst, 'metadata', {}) or {}).get('Properties', '')]
 
     @staticmethod
     def list_volumes():
+        conn = OpenStack().conn
         ownedby = "ownedby='" + teuth_config.openstack['ip'] + "'"
-        all = json.loads(OpenStack().run("volume list -f json --long"))
+        volumes = conn.block_storage.volumes()
         def select(volume):
-            return (ownedby in volume['Properties'] and
-                    volume['Display Name'].startswith('target'))
-        return filter(select, all)
+            props = volume.metadata or {}
+            return (ownedby in props.get('Properties', '') and
+                    props.get('display_name', '').startswith('target'))
+        return filter(select, volumes)
 
     def cloud_init_wait(self, instance):
         """
@@ -564,12 +569,11 @@ class OpenStack(object):
         with safe_while(sleep=30, tries=30,
                         action="cloud_init_wait " + ip) as proceed:
             success = False
-            # CentOS 6.6 logs in /var/log/clout-init-output.log
-            # CentOS 7.0 logs in /var/log/clout-init.log
             tail = ("tail --follow=name --retry"
                         " /var/log/cloud-init*.log /tmp/init.out")
             while proceed():
                 try:
+                    log.debug("Attempting to connect to instance at IP: " + ip)
                     client = connection.connect(**client_args)
                 except paramiko.PasswordRequiredException:
                     raise Exception(
@@ -611,15 +615,17 @@ class OpenStack(object):
                             break
                 except socket.timeout:
                     client.close()
-                    log.debug('cloud_init_wait socket.timeout ' + tail)
                     continue
-                except socket.error as e:
+                except socket.error:
                     client.close()
-                    log.debug('cloud_init_wait socket.error ' + str(e) + ' ' + tail)
                     continue
-                client.close()
+                finally:
+                    client.close()
                 if success:
+                    log.debug('Cloud-init completed successfully for IP: ' + ip)
                     break
+            if not success:
+                log.debug('Cloud-init did not complete successfully within the given retries.')
             return success
 
     def get_ip(self, instance_id, network):
@@ -899,7 +905,7 @@ class TeuthologyOpenStack(OpenStack):
         if ceph_repo:
             command = (
                 "perl -pi -e 's|.*{opt}.*|{opt}: {value}|'"
-                " ~/.teuthology.yaml"
+                " ~/.teuthology.yaml || true"
             ).format(opt='ceph_git_url', value=ceph_repo)
             self.ssh(command)
         user_home = '/home/' + self.username
@@ -914,6 +920,7 @@ class TeuthologyOpenStack(OpenStack):
             " --machine-type openstack " +
             " ".join(map(lambda x: "'" + x + "'", argv))
         )
+        log.info("Running teuthology-suite: " + command)
         return self.ssh(command)
 
     def reminders(self):
@@ -951,7 +958,7 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
             logging.getLogger("paramiko.transport").setLevel(logging.DEBUG)
         teuthology.log.setLevel(loglevel)
 
-    def ssh(self, command):
+    def ssh(self, command, timeout=300):
         """
         Run a command in the OpenStack instance of the teuthology cluster.
         Return the stdout / stderr of the command.
@@ -969,14 +976,27 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
         # get the I/O channel to iterate line by line
         transport = client.get_transport()
         channel = transport.open_session()
-        channel.get_pty()
-        channel.settimeout(900)
-        output = channel.makefile('r', 1)
-        log.debug(":ssh@" + ip + ":" + command)
+        channel.settimeout(timeout)
+        log.debug(f"ssh {self.instance.get_floating_ip_or_ip()}: {command}")
         channel.exec_command(command)
-        for line in iter(output.readline, b''):
-            log.info(line.strip())
-        return channel.recv_exit_status()
+        stdout, stderr = [], []
+        start_time = time.time()
+        while True:
+            if channel.recv_ready():
+                stdout.append(channel.recv(4096).decode())
+            if channel.recv_stderr_ready():
+                stderr.append(channel.recv_stderr(4096).decode())
+            if channel.exit_status_ready():
+                break
+            if time.time() - start_time > timeout:
+                raise TimeoutError("SSH command timed out!")
+            time.sleep(0.1)  # Small sleep to avoid busy waiting
+        exit_status = channel.recv_exit_status()
+        stdout_txt, stderr_txt = ''.join(stdout), ''.join(stderr)
+        if exit_status != 0:
+            log.warning(f"SSH command failed with exit status {exit_status}")
+        return exit_status, stdout_txt, stderr_txt
+
 
     def verify_openstack(self):
         """
@@ -1029,32 +1049,48 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
         fd, path = tempfile.mkstemp()
         os.close(fd)
 
-        with open(os.path.dirname(__file__) + '/bootstrap-teuthology.sh', 'rb') as f:
+        bootstrap_path = os.getcwd() + "/teuthology/openstack" + '/bootstrap-teuthology.sh'
+        with open(bootstrap_path, 'rb') as f:
             b64_bootstrap = base64.b64encode(f.read())
             bootstrap_content = str(b64_bootstrap.decode())
 
         openrc_sh = ''
         cacert_cmd = None
-        for (var, value) in os.environ.items():
-            if var in ('OS_TOKEN_VALUE', 'OS_TOKEN_EXPIRES'):
-                continue
-            if var == 'OS_CACERT':
-                cacert_path = '/home/%s/.openstack.crt' % self.username
-                cacert_file = value
-                openrc_sh += 'export %s=%s\n' % (var, cacert_path)
-                cacert_cmd = (
-                    "su - -c 'cat > {path}' {user} <<EOF\n"
-                    "{data}\n"
-                    "EOF\n").format(
-                        path=cacert_path,
-                        user=self.username,
-                        data=open(cacert_file).read())
-            elif var.startswith('OS_'):
-                openrc_sh += 'export %s=%s\n' % (var, value)
+        clouds_yaml_path = os.path.expanduser('~/.config/openstack/clouds.yaml')
+        if os.path.exists(clouds_yaml_path):
+            log.debug(f"clouds.yaml found at {clouds_yaml_path}, processing for openrc.sh")
+            with open(clouds_yaml_path, 'r') as f:
+                clouds_data = yaml.safe_load(f)
+            cloud_name = os.environ.get('OS_CLOUD', 'default')
+            cloud_config = clouds_data.get('clouds', {}).get(cloud_name, {})
+            if not cloud_config:
+                raise Exception(f"Cloud '{cloud_name}' not found in clouds.yaml")
+            auth = cloud_config.get('auth', {})
+            for key, value in {**auth, **cloud_config}.items():
+                if isinstance(value, str):
+                    openrc_sh += f"export OS_{key.upper()}={value}\n"
+        else:
+            for (var, value) in os.environ.items():
+                if var in ('OS_TOKEN_VALUE', 'OS_TOKEN_EXPIRES'):
+                    continue
+                if var == 'OS_CACERT':
+                    cacert_path = '/home/%s/.openstack.crt' % self.username
+                    cacert_file = value
+                    openrc_sh += 'export %s=%s\n' % (var, cacert_path)
+                    cacert_cmd = (
+                        "su - -c 'cat > {path}' {user} <<EOF\n"
+                        "{data}\n"
+                        "EOF\n").format(
+                            path=cacert_path,
+                            user=self.username,
+                            data=open(cacert_file).read())
+                elif var.startswith('OS_'):
+                    openrc_sh += 'export %s=%s\n' % (var, value)
         b64_openrc_sh = base64.b64encode(openrc_sh.encode())
         openrc_sh_content = str(b64_openrc_sh.decode())
 
         network = OpenStack().get_network()
+        log.debug(f"Network to be used: {network}")
         ceph_workbench = ''
         if self.args.ceph_workbench_git_url:
             ceph_workbench += (" --ceph-workbench-branch " +
@@ -1069,21 +1105,19 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
             '--server-group %s' % self.server_group(),
             '--worker-group %s' % self.worker_group(),
             '--package-repo %s' % self.packages_repository(),
-            #'--setup-all',
         ]
+        log.debug(f"Setup options: {setup_options}")
+
         all_options = [
-            '--install',                #do_install_packages=true
-            #'--setup-ceph-workbench',   #do_ceph_workbench=true
-            '--config',                 #do_create_config=true
-            '--setup-keypair',          #do_setup_keypair=true
-            #'',           #do_apt_get_update=true
-            '--setup-docker',           #do_setup_docker=true
-            '--setup-salt-master',      #do_setup_salt_master=true
-            '--setup-dnsmasq',          #do_setup_dnsmasq=true
-            '--setup-fail2ban',         #do_setup_fail2ban=true
-            '--setup-paddles',          #do_setup_paddles=true
-            '--setup-pulpito',          #do_setup_pulpito=true
-            '--populate-paddles',       #do_populate_paddles=true
+            '--install',
+            '--config',
+            '--setup-docker',
+            '--setup-salt-master',
+            '--setup-dnsmasq',
+            '--setup-fail2ban',
+            '--setup-paddles',
+            '--setup-pulpito',
+            '--populate-paddles',
         ]
 
         if self.args.ceph_workbench_git_url:
@@ -1122,8 +1156,6 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
                 "{user} >> /tmp/init.out "
                 "2>&1".format(user=self.username,
                               opts=' '.join(setup_options + all_options))),
-            # wa: we want to stop paddles and pulpito started by
-            # setup-openstack before starting teuthology service
             "pkill -f 'pecan serve'",
             "pkill -f 'python run.py'",
             "systemctl enable teuthology",
@@ -1143,11 +1175,6 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
                     'name': self.username
                 }
             },
-            'packages': [
-                'python-virtualenv',
-                'git',
-                'rsync',
-            ],
             'write_files': [
                 {
                     'path': '/tmp/bootstrap-teuthology.sh',
@@ -1163,7 +1190,9 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
                     'permissions': '0644',
                 }
             ],
-            'runcmd': cmds,
+            'runcmd': [
+                'apt-get update && apt-get install -y python3-virtualenv git rsync >> /tmp/init.out 2>&1'
+            ] + cmds,
             'final_message': 'teuthology is up and running after $UPTIME seconds'
         }
         user_data = "#cloud-config\n%s" % \
@@ -1186,36 +1215,41 @@ ssh access           : ssh {identity}{username}@{ip} # logs in /usr/share/nginx/
         return "teuth-%s-worker" % self.args.name
 
     def create_security_group(self):
-        """
-        Create a security group that will be used by all teuthology
-        created instances. This should not be necessary in most cases
-        but some OpenStack providers enforce firewall restrictions even
-        among instances created within the same tenant.
-        """
-        groups = misc.sh('openstack security group list -c Name -f value').split('\n')
-        if all(g in groups for g in [self.server_group(), self.worker_group()]):
-            return
-        misc.sh("""
-openstack security group delete {server} || true
-openstack security group delete {worker} || true
-openstack security group create {server}
-openstack security group create {worker}
-# access to teuthology VM from the outside
-openstack security group rule create --proto tcp --dst-port 22 {server} # ssh
-openstack security group rule create --proto tcp --dst-port 80 {server} # for log access
-openstack security group rule create --proto tcp --dst-port 8080 {server} # pulpito
-openstack security group rule create --proto tcp --dst-port 8081 {server} # paddles
-# access between teuthology and workers
-openstack security group rule create --src-group {worker} --dst-port 1:65535 {server}
-openstack security group rule create --protocol udp --src-group {worker} --dst-port 1:65535 {server}
-openstack security group rule create --src-group {server} --dst-port 1:65535 {worker}
-openstack security group rule create --protocol udp --src-group {server} --dst-port 1:65535 {worker}
-# access between members of one group
-openstack security group rule create --src-group {worker} --dst-port 1:65535 {worker}
-openstack security group rule create --protocol udp --src-group {worker} --dst-port 1:65535 {worker}
-openstack security group rule create --src-group {server} --dst-port 1:65535 {server}
-openstack security group rule create --protocol udp --src-group {server} --dst-port 1:65535 {server}
-        """.format(server=self.server_group(), worker=self.worker_group()))
+        conn = OpenStack().conn
+        server_sg = conn.network.find_security_group(self.server_group())
+        worker_sg = conn.network.find_security_group(self.worker_group())
+        if not server_sg:
+            server_sg = conn.network.create_security_group(name=self.server_group())
+        if not worker_sg:
+            worker_sg = conn.network.create_security_group(name=self.worker_group())
+        def add_rule(sg_id, protocol, port, remote_group_id=None):
+            rule_args = {
+                'security_group_id': sg_id,
+                'direction': 'ingress',
+                'protocol': protocol,
+                'port_range_min': port,
+                'port_range_max': port,
+                'ethertype': 'IPv4',
+            }
+            if remote_group_id:
+                rule_args['remote_group_id'] = remote_group_id
+            else:
+                rule_args['remote_ip_prefix'] = '0.0.0.0/0'
+            try:
+                conn.network.create_security_group_rule(**rule_args)
+            except Exception as e:
+                log.warning(f"Security group rule creation skipped or failed: {e}")
+        # Rules for SSH, log, pulpito and paddles
+        for port in (22, 80, 8080, 8081):
+            add_rule(server_sg.id, 'tcp', port)
+        # access between teuthology and workers
+        for port in (65535,):
+            add_rule(worker_sg.id, 'udp', port, remote_group_id=server_sg.id)
+            add_rule(server_sg.id, 'udp', port, remote_group_id=worker_sg.id)
+        # access between members of one group
+        add_rule(server_sg.id, 'udp', 65535, remote_group_id=server_sg.id)
+        # access within worker group
+        add_rule(worker_sg.id, 'udp', 65535, remote_group_id=worker_sg.id)
 
     @staticmethod
     def get_unassociated_floating_ip():
@@ -1230,32 +1264,14 @@ openstack security group rule create --protocol udp --src-group {server} --dst-p
 
     @staticmethod
     def create_floating_ip():
-        try:
-            pools = json.loads(OpenStack().run("ip floating pool list -f json"))
-        except subprocess.CalledProcessError as e:
-            if 'Floating ip pool operations are only available for Compute v2 network.' \
-                    in e.output:
-                log.debug(e.output)
-                log.debug('Trying newer API than Compute v2')
-                try:
-                    network = 'floating'
-                    ip = json.loads(misc.sh("openstack --quiet floating ip create -f json '%s'" % network))
-                    return ip['floating_ip_address']
-                except subprocess.CalledProcessError:
-                    log.debug("Can't create floating ip for network '%s'" % network)
-
-            log.debug("create_floating_ip: ip floating pool list failed")
-            return None
-        if not pools:
+        conn = OpenStack().conn
+        network_name = 'floating'
+        network = conn.network.find_network(network_name)
+        if not network:
+            log.debug(f"Floating network {network_name} not found.")
             return None
-        pool = pools[0]['Name']
-        try:
-            ip = json.loads(OpenStack().run(
-                "ip floating create -f json '" + pool + "'"))
-            return ip['ip']
-        except subprocess.CalledProcessError:
-            log.debug("create_floating_ip: not creating a floating ip")
-        return None
+        floating_ip = conn.network.create_ip(floating_network_id=network.id)
+        return floating_ip.floating_ip_address
 
     @staticmethod
     def associate_floating_ip(name_or_id):
@@ -1263,23 +1279,18 @@ openstack security group rule create --protocol udp --src-group {server} --dst-p
         Associate a floating IP to the OpenStack instance
         or do nothing if no floating ip can be created.
         """
-        ip = TeuthologyOpenStack.get_unassociated_floating_ip()
-        if not ip:
-            ip = TeuthologyOpenStack.create_floating_ip()
-        if ip:
-            OpenStack().run("ip floating add " + ip + " " + name_or_id)
+        conn = OpenStack().conn
+        server = conn.compute.find_server(name_or_id)
+        ip_address = TeuthologyOpenStack.get_unassociated_floating_ip()
+        if not ip_address:
+            ip_address = TeuthologyOpenStack.create_floating_ip()
+        if ip_address:
+            conn.compute.add_floating_ip_to_server(server, ip_address)
 
     @staticmethod
     def get_os_floating_ips():
-        try:
-            ips = json.loads(OpenStack().run("ip floating list -f json"))
-        except subprocess.CalledProcessError as e:
-            log.warning(e)
-            if e.returncode == 1:
-                return []
-            else:
-                raise e
-        return ips
+        conn = OpenStack().conn
+        return list(conn.network.ips())
 
     @staticmethod
     def get_floating_ip_id(ip):
@@ -1307,12 +1318,17 @@ openstack security group rule create --protocol udp --src-group {server} --dst-p
         """
         Remove the floating ip from instance_id and delete it.
         """
-        ip = OpenStackInstance(instance_id).get_floating_ip()
-        if not ip:
+        conn = OpenStack().conn
+        server = conn.compute.find_server(instance_id)
+        if not server:
+            return
+        ip_address = OpenStackInstance(instance_id).get_floating_ip()
+        if not ip_address:
             return
-        OpenStack().run("ip floating remove " + ip + " " + instance_id)
-        ip_id = TeuthologyOpenStack.get_floating_ip_id(ip)
-        OpenStack().run("ip floating delete " + ip_id)
+        conn.compute.remove_floating_ip_from_server(server, ip_address)
+        floating_ip_obj = conn.network.find_ip(ip_address)
+        if floating_ip_obj:
+            conn.network.delete_ip(floating_ip_obj)
 
     def create_cluster(self):
         user_data = self.get_user_data()
@@ -1329,19 +1345,29 @@ openstack security group rule create --protocol udp --src-group {server} --dst-p
         if not key_name:
             raise Exception('No key name provided, use --key-name option')
         log.debug('Using key name: %s' % self.args.key_name)
-        self.run(
+        image_name = self.image('ubuntu', '22.04', arch)
+        log.debug("Using image: %s" % image_name)
+        net_config = self.net()
+        try:
+            self.run(
             "server create " +
-            " --image '" + self.image('ubuntu', '16.04', arch) + "' " +
+            " --image '" + image_name + "' " +
             " --flavor '" + flavor + "' " +
-            " " + self.net() +
+            " " + net_config +
             " --key-name " + key_name +
             " --user-data " + user_data +
             security_group +
             " --wait " + self.server_name() +
             " -f json")
-        os.unlink(user_data)
+        except Exception as e:
+            log.error("Error during server creation: %s" % str(e))
+            raise
+        finally:
+            os.unlink(user_data)
         self.instance = OpenStackInstance(self.server_name())
+        log.debug("OpenStackInstance created for server: %s" % self.server_name())
         self.associate_floating_ip(self.instance['id'])
+        log.debug("Floating IP associated for instance ID: %s" % self.instance.get('id'))
         return self.cloud_init_wait(self.instance)
 
     def packages_repository(self):
index b6d9aa1b090bfd5ae1bffc82690addff8777d5e6..526ba98136214ab8fa53f50e8b0366c5e4dc7483 100755 (executable)
@@ -41,6 +41,8 @@ function create_config() {
     local server_group="${11}"
     local worker_group="${12}"
     local package_repo="${13}"
+    local teuthology_branch="${14}"
+    local teuthology_git_url="${15}"
 
     if test "$network" ; then
         network="network: $network"
@@ -65,6 +67,8 @@ queue_host: localhost
 lab_domain: $labdomain
 max_job_time: 32400 # 9 hours
 teuthology_path: .
+teuthology_branch: $teuthology_branch
+teuthology_git_url: $teuthology_git_url
 canonical_tags: $canonical_tags
 openstack:
   clone: git clone http://github.com/ceph/teuthology
@@ -113,13 +117,23 @@ function apt_get_update() {
 }
 
 function setup_docker() {
-    if test -f /etc/apt/sources.list.d/docker.list ; then
-        echo "OK docker is installed"
-    else
-        sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
-        echo deb https://apt.dockerproject.org/repo ubuntu-trusty main | sudo tee -a /etc/apt/sources.list.d/docker.list
-        sudo apt-get -qq install -y docker-engine
+    source /etc/os-release
+    if ! $VERSION_CODENAME; then
+        echo "ERROR: VERSION_CODENAME is not set. Cannot proceed with Docker installation."
+        return
+    fi
+    if !command -v docker &> /dev/null; then
+        curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
+            sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+        echo \
+          "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
+          https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" | \
+          sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+        sudo apt-get update
+        sudo apt-get install -y docker-ce docker-ce-cli containerd.io
         echo "INSTALLED docker"
+    else
+        echo "OK docker is installed"
     fi
 }
 
@@ -163,7 +177,7 @@ function setup_paddles() {
         git clone https://github.com/ceph/paddles.git $paddles_dir || return 1
     fi
 
-    sudo apt-get -qq install -y --force-yes beanstalkd postgresql postgresql-contrib postgresql-server-dev-all supervisor
+    sudo apt-get -qq install -y beanstalkd postgresql postgresql-contrib postgresql-server-dev-all supervisor
 
     if ! sudo /etc/init.d/postgresql status ; then
         sudo mkdir -p /etc/postgresql
@@ -179,10 +193,10 @@ function setup_paddles() {
         cd $paddles_dir || return 1
         git pull --rebase
         git clean -ffqdx
-        sed -e "s|^address.*|address = 'http://localhost'|" \
-            -e "s|^job_log_href_templ = 'http://qa-proxy.ceph.com/teuthology|job_log_href_templ = 'http://$public_ip|" \
+        sed -e "/^address = os.environ.get(/,/^)/c\address = os.environ.get('PADDLES_ADDRESS', 'http://localhost')" \
+            -e "s|^job_log_href_templ = os.environ.get(.*)|job_log_href_templ = os.environ.get('PADDLES_JOB_LOG_HREF_TEMPL', 'http://$public_ip')|" \
             -e "/sqlite/d" \
-            -e "s|.*'postgresql+psycop.*'|'url': 'postgresql://paddles:paddles@localhost/paddles'|" \
+            -e "s|^ *'url':.*|'url': 'postgresql://paddles:paddles@localhost/paddles',|" \
             -e "s/'host': '127.0.0.1'/'host': '0.0.0.0'/" \
             < config.py.in > config.py
         virtualenv ./virtualenv
@@ -258,7 +272,7 @@ function setup_pulpito() {
         git clone https://github.com/ceph/pulpito.git $pulpito_dir || return 1
     fi
 
-    sudo apt-get -qq install -y --force-yes nginx
+    sudo apt-get -qq install -y nginx
     local nginx_conf=/etc/nginx/sites-available/default
     sudo sed -i '/text\/plain/a\    text\/plain                            log;' \
         /etc/nginx/mime.types
@@ -278,7 +292,7 @@ function setup_pulpito() {
         virtualenv ./virtualenv
         source ./virtualenv/bin/activate
         pip install --upgrade pip
-        pip install 'setuptools==18.2.0'
+        pip install 'setuptools>=58.0.0'
         pip install -r requirements.txt
         python run.py &
     )
@@ -408,7 +422,7 @@ function setup_dnsmasq() {
 
     if ! test -f /etc/dnsmasq.d/resolv ; then
         resolver=$(grep nameserver /etc/resolv.conf | head -1 | perl -ne 'print $1 if(/\s*nameserver\s+([\d\.]+)/)')
-        sudo apt-get -qq install -y --force-yes dnsmasq resolvconf
+        sudo apt-get -qq install -y dnsmasq resolvconf
         # FIXME: this opens up dnsmasq to DNS reflection/amplification attacks, and can be reverted
         # FIXME: once we figure out how to configure dnsmasq to accept DNS queries from all subnets
         sudo perl -pi -e 's/--local-service//' /etc/init.d/dnsmasq
@@ -500,22 +514,27 @@ function remove_images() {
 }
 
 function install_packages() {
-
-    if ! test -f /etc/apt/sources.list.d/trusty-backports.list ; then
-        echo deb http://archive.ubuntu.com/ubuntu trusty-backports main universe | sudo tee /etc/apt/sources.list.d/trusty-backports.list
+    source /etc/os-release
+    if ! $VERSION_CODENAME; then
+        echo "ERROR: VERSION_CODENAME is not set. Cannot proceed with Docker installation."
+        return
+    fi
+    local codename=$VERSION_CODENAME
+    local backports_file="/etc/apt/sources.list.d/${codename}-backports.list"
+    if [ ! -f "$backports_file" ]; then
+        echo "Adding backports repo for $codename..."
+        echo "deb http://archive.ubuntu.com/ubuntu ${codename}-backports main universe" | sudo tee "$backports_file"
         sudo apt-get update
     fi
-
-    local packages="jq realpath curl"
-    sudo apt-get -qq install -y --force-yes $packages
-
+    local packages="jq curl"
+    sudo apt-get -qq install -y $packages
     echo "INSTALL required packages $packages"
 }
 
 CAT=${CAT:-cat}
 
 function verify_openstack() {
-    if ! openstack server list > /dev/null ; then
+    if ! openstack server list --format json > /dev/null ; then
         echo ERROR: the credentials from ~/openrc.sh are not working >&2
         return 1
     fi
@@ -701,13 +720,14 @@ function main() {
     # assume the first available IPv4 subnet is going to be used to assign IP to the instance
     #
     [ -z "$network" ] && {
-        local default_subnets=$(openstack subnet list --ip-version 4 -f json | jq -r '.[] | .Subnet' | sort | uniq)
+        local default_subnets=$(openstack subnet list --ip-version 4 -f json | jq -r '.[] | select(.Name != null) | .Subnet' | sort | uniq)
     } || {
         local network_id=$(openstack network list -f json | jq -r ".[] | select(.Name == \"$network\") | .ID")
         local default_subnets=$(openstack subnet list --ip-version 4 -f json \
             | jq -r ".[] | select(.Network == \"$network_id\") | .Subnet" | sort | uniq)
     }
     subnets=$(echo $subnets $default_subnets)
+    echo "subnets: $subnets"
 
     case $provider in
         entercloudsuite)
@@ -720,16 +740,21 @@ function main() {
     esac
 
     local ip
-    for dev in eth0 ens3 ; do
-        ip=$(ip a show dev $dev 2>/dev/null | sed -n "s:.*inet \(.*\)/.*:\1:p")
-        test "$ip" && break
+    for dev in $(ip -o link show | awk -F': ' '{print $2}' | grep -v '^lo$'); do
+        ip=$(ip -4 addr show dev "$dev" | awk '/inet / {print $2}' | cut -d/ -f1)
+        if [ -n "$ip" ]; then
+            nameserver="$ip"
+            break
+        fi
     done
-    : ${nameserver:=$ip}
+
+    local teuthology_branch="$(git -C $(dirname $0)/../../../teuthology rev-parse --abbrev-ref HEAD)"
+    local teuthology_git_url="$(git -C $(dirname $0)/../../../teuthology config --get remote.origin.url)"
 
     if $do_create_config ; then
         create_config "$network" "$subnets" "$nameserver" "$labdomain" "$ip" \
             "$archive_upload" "$canonical_tags" "$selfname" "$keypair" \
-            "$server_name" "$server_group" "$worker_group" "$package_repo" || return 1
+            "$server_name" "$server_group" "$worker_group" "$package_repo" "$teuthology_branch" "$teuthology_git_url" || return 1
         setup_ansible "$subnets" $labdomain || return 1
         setup_ssh_config || return 1
         setup_authorized_keys || return 1
index ba72a4334c4724ba8e43e7d61779bcad9daa3164..335df630498d0063f5bd7c61e7cd5113c534eb99 100644 (file)
@@ -7,6 +7,8 @@ import yaml
 import re
 import time
 
+from pathlib import Path
+
 from humanfriendly import format_timespan
 
 from teuthology import repo_utils
@@ -305,7 +307,7 @@ class Run(object):
             if not teuthology_branch:
                 teuthology_branch = actual_branch
             teuthology_sha1 = util.git_ls_remote(
-                f"file://{config.teuthology_path}",
+                f"file://{Path(config.teuthology_path).resolve()}",
                 teuthology_branch
             )
         else: