]> git.apps.os.sepia.ceph.com Git - teuthology.git/commitdiff
openstack: introduce OpenStackInstance
authorLoic Dachary <ldachary@redhat.com>
Sun, 22 Nov 2015 22:57:30 +0000 (23:57 +0100)
committerLoic Dachary <ldachary@redhat.com>
Tue, 1 Dec 2015 11:19:23 +0000 (12:19 +0100)
To reduce the number of redundant calls and reduce the code complexity.

Signed-off-by: Loic Dachary <loic@dachary.org>
teuthology/openstack/__init__.py
teuthology/openstack/test/test_openstack.py
teuthology/provision.py

index 36ab6826508d8dcaca3d02fb2d2ffac147ede480..8ed0b0b3ea360bcca7164b67b94d4af4a487a6d3 100644 (file)
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 import copy
+import datetime
 import json
 import logging
 import os
@@ -42,6 +43,111 @@ from teuthology import misc
 
 log = logging.getLogger(__name__)
 
+class OpenStackInstance(object):
+
+    def __init__(self, name_or_id, info=None):
+        self.name_or_id = name_or_id
+        if info is None:
+            self.set_info()
+        else:
+            self.info = dict(map(lambda (k,v): (k.lower(), v), info.iteritems()))
+
+    def set_info(self):
+        try:
+            info = json.loads(
+                misc.sh("openstack server show -f json " + self.name_or_id))
+            self.info = dict(map(
+                lambda p: (p['Field'].lower(), p['Value']), info))
+        except CalledProcessError:
+            self.info = None
+
+    def __getitem__(self, name):
+        return self.info[name.lower()]
+
+    def get_created(self):
+        now = datetime.datetime.now()
+        created = datetime.datetime.strptime(
+            self['created'], '%Y-%m-%dT%H:%M:%SZ')
+        return (now - created).total_seconds()
+
+    def exists(self):
+        return self.info is not None
+
+    def get_volumes(self):
+        """
+        Return the uuid of the volumes attached to the name_or_id
+        OpenStack instance.
+        """
+        volumes = self['os-extended-volumes:volumes_attached']
+        return [volume['id'] for volume in volumes ]
+
+    def get_addresses(self):
+        """
+        Return the list of IPs associated with instance_id in OpenStack.
+        """
+        with safe_while(sleep=2, tries=30,
+                        action="get ip " + self['id']) as proceed:
+            while proceed():
+                found = re.match('.*\d+', self['addresses'])
+                if found:
+                    return self['addresses']
+                self.set_info()
+
+    def get_ip_neutron(self):
+        subnets = json.loads(misc.sh("neutron subnet-list -f json -c id -c ip_version"))
+        subnet_id = None
+        for subnet in subnets:
+            if subnet['ip_version'] == 4:
+                subnet_id = subnet['id']
+                break
+        if not subnet_id:
+            raise Exception("no subnet with ip_version == 4")
+        ports = json.loads(misc.sh("neutron port-list -f json -c fixed_ips -c device_id"))
+        fixed_ips = None
+        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'] == subnet_id:
+                ip = record['ip_address']
+                break
+        if not ip:
+            raise Exception("no ip")
+        return ip
+
+    def get_ip(self, network):
+        """
+        Return the private IP of the OpenStack instance_id.
+        """
+        try:
+            return self.get_ip_neutron()
+        except Exception as e:
+            log.debug("ignoring get_ip_neutron exception " + str(e))
+            return re.findall(network + '=([\d.]+)',
+                              self.get_addresses())[0]
+
+    def destroy(self):
+        """
+        Delete the name_or_id OpenStack instance.
+        """
+        if not self.exists():
+            return True
+        volumes = self.get_volumes()
+        misc.sh("openstack server set --name REMOVE-ME-" + self.name_or_id +
+                " " + self['id'])
+        misc.sh("openstack server delete --wait " + self['id'] +
+                " || true")
+        for volume in volumes:
+            misc.sh("openstack volume set --name REMOVE-ME " + volume)
+            misc.sh("openstack volume delete " + volume + " || true")
+        return True
+
+
 class OpenStack(object):
 
     # wget -O debian-8.0.qcow2  http://cdimage.debian.org/cdimage/openstack/current/debian-8.1.0-openstack-amd64.qcow2
@@ -201,6 +307,23 @@ class OpenStack(object):
                         current[key] = max(current[key], new[key])
         return result
 
+    @staticmethod
+    def list_instances():
+        ownedby = "ownedby='" + teuth_config.openstack['ip'] + "'"
+        all = json.loads(misc.sh(
+            "openstack server list -f json --long --name 'target'"))
+        return filter(lambda instance: ownedby in instance['Properties'], all)
+
+    @staticmethod
+    def list_volumes():
+        ownedby = "ownedby='" + teuth_config.openstack['ip'] + "'"
+        all = json.loads(misc.sh(
+            "openstack volume list -f json --long"))
+        def select(volume):
+            return (ownedby in volume['Properties'] and
+                    volume['Display Name'].startswith('target'))
+        return filter(select, all)
+
     def cloud_init_wait(self, name_or_ip):
         """
         Wait for cloud-init to complete on the name_or_ip OpenStack instance.
@@ -273,93 +396,8 @@ class OpenStack(object):
                     break
             return success
 
-    @staticmethod
-    def show(name_or_id):
-        """
-        Run "openstack server show -f json <name_or_id>" and return the result.
-
-        Does not handle exceptions.
-        """
-        try:
-            return json.loads(
-                misc.sh("openstack server show -f json %s" % name_or_id)
-            )
-        except CalledProcessError:
-            return False
-
-    @classmethod
-    def exists(cls, name_or_id, server_info=None):
-        """
-        Return true if the OpenStack name_or_id instance exists,
-        false otherwise.
-
-        :param name_or_id:  The name or ID of the server to query
-        :param server_info: Optionally, use already-retrieved results of
-                            self.show()
-        """
-        if server_info is None:
-            server_info = cls.show(name_or_id)
-        if not server_info:
-            return False
-        if (cls.get_value(server_info, 'Name') == name_or_id or
-                cls.get_value(server_info, 'ID') == name_or_id):
-            return True
-        return False
-
-    @staticmethod
-    def get_addresses(instance_id):
-        """
-        Return the list of IPs associated with instance_id in OpenStack.
-        """
-        with safe_while(sleep=2, tries=30,
-                        action="get ip " + instance_id) as proceed:
-            while proceed():
-                instance = misc.sh("openstack server show -f json " +
-                                   instance_id)
-                addresses = OpenStack.get_value(json.loads(instance),
-                                                'addresses')
-                found = re.match('.*\d+', addresses)
-                if found:
-                    return addresses
-
-    @staticmethod
-    def get_ip_neutron(instance_id):
-        subnets = json.loads(misc.sh("neutron subnet-list -f json -c id -c ip_version"))
-        subnet_id = None
-        for subnet in subnets:
-            if subnet['ip_version'] == 4:
-                subnet_id = subnet['id']
-                break
-        if not subnet_id:
-            raise Exception("no subnet with ip_version == 4")
-        ports = json.loads(misc.sh("neutron port-list -f json -c fixed_ips -c device_id"))
-        fixed_ips = None
-        for port in ports:
-            if port['device_id'] == instance_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'] == subnet_id:
-                ip = record['ip_address']
-                break
-        if not ip:
-            raise Exception("no ip")
-        return ip
-
     def get_ip(self, instance_id, network):
-        """
-        Return the private IP of the OpenStack instance_id.
-        """
-        try:
-            return self.get_ip_neutron(instance_id)
-        except Exception as e:
-            log.debug("ignoring get_ip_neutron exception " + str(e))
-            return re.findall(network + '=([\d.]+)',
-                              self.get_addresses(instance_id))[0]
+        return OpenStackInstance(instance_id).get_ip(network)
 
 
 class TeuthologyOpenStack(OpenStack):
@@ -660,13 +698,12 @@ openstack security group rule create --proto udp --dst-port 53 teuthology # dns
         ip = TeuthologyOpenStack.get_floating_ip(instance_id)
         if not ip:
             ip = re.findall('([\d.]+)$',
-                            TeuthologyOpenStack.get_addresses(instance_id))[0]
+                            OpenStackInstance(instance_id).get_addresses())[0]
         return ip
 
     @staticmethod
     def get_instance_id(name):
-        instance = json.loads(misc.sh("openstack server show -f json " + name))
-        return TeuthologyOpenStack.get_value(instance, 'id')
+        return OpenStackInstance(name)['id']
 
     @staticmethod
     def delete_floating_ip(instance_id):
@@ -710,10 +747,10 @@ openstack security group rule create --proto udp --dst-port 53 teuthology # dns
         """
         Return true if there exists an instance running the teuthology cluster.
         """
-        if not self.exists(self.args.name):
+        instance = OpenStackInstance(self.args.name)
+        if not instance.exists():
             return False
-        instance_id = self.get_instance_id(self.args.name)
-        ip = self.get_floating_ip_or_ip(instance_id)
+        ip = self.get_floating_ip_or_ip(instance['id'])
         return self.cloud_init_wait(ip)
 
     def teardown(self):
index feebaa0c8494cb5a3e87bf2dc98da62d6b9a57ea..32fecb5b7265a2895249921a0312e2e7dba5dec9 100644 (file)
@@ -25,14 +25,124 @@ import argparse
 import logging
 import os
 import pytest
+import subprocess
 import tempfile
 from mock import patch
 
 import teuthology
 from teuthology import misc
-from teuthology.openstack import TeuthologyOpenStack, OpenStack
+from teuthology.openstack import TeuthologyOpenStack, OpenStack, OpenStackInstance
 import scripts.openstack
 
+
+class TestOpenStackInstance(object):
+
+    teuthology_instance = '[{"Field": "OS-DCF:diskConfig", "Value": "MANUAL"}, {"Field": "OS-EXT-AZ:availability_zone", "Value": "nova"}, {"Field": "OS-EXT-STS:power_state", "Value": 1}, {"Field": "OS-EXT-STS:task_state", "Value": null}, {"Field": "OS-EXT-STS:vm_state", "Value": "active"}, {"Field": "OS-SRV-USG:launched_at", "Value": "2015-11-12T14:18:42.000000"}, {"Field": "OS-SRV-USG:terminated_at", "Value": null}, {"Field": "accessIPv4", "Value": ""}, {"Field": "accessIPv6", "Value": ""}, {"Field": "addresses", "Value": "Ext-Net=167.114.233.32"}, {"Field": "config_drive", "Value": ""}, {"Field": "created", "Value": "2015-11-12T14:18:22Z"}, {"Field": "flavor", "Value": "eg-30 (3c1d6170-0097-4b5c-a3b3-adff1b7a86e0)"}, {"Field": "hostId", "Value": "b482bcc97b6b2a5b3569dc349e2b262219676ddf47a4eaf72e415131"}, {"Field": "id", "Value": "f3ca32d7-212b-458b-a0d4-57d1085af953"}, {"Field": "image", "Value": "teuthology-ubuntu-14.04 (4300a7ca-4fbd-4b34-a8d5-5a4ebf204df5)"}, {"Field": "key_name", "Value": "myself"}, {"Field": "name", "Value": "teuthology"}, {"Field": "os-extended-volumes:volumes_attached", "Value": [{"id": "627e2631-fbb3-48cd-b801-d29cd2a76f74"}, {"id": "09837649-0881-4ee2-a560-adabefc28764"}, {"id": "44e5175b-6044-40be-885a-c9ddfb6f75bb"}]}, {"Field": "progress", "Value": 0}, {"Field": "project_id", "Value": "131b886b156a4f84b5f41baf2fbe646c"}, {"Field": "properties", "Value": ""}, {"Field": "security_groups", "Value": [{"name": "teuthology"}]}, {"Field": "status", "Value": "ACTIVE"}, {"Field": "updated", "Value": "2015-11-12T14:18:42Z"}, {"Field": "user_id", "Value": "291dde1633154837be2693c6ffa6315c"}]'
+
+    teuthology_instance_no_addresses = '[{"Field": "addresses", "Value": ""}, {"Field": "id", "Value": "f3ca32d7-212b-458b-a0d4-57d1085af953"}]'
+
+    def test_init(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o['id'] == 'f3ca32d7-212b-458b-a0d4-57d1085af953'
+        o = OpenStackInstance('NAME', {"id": "OTHER"})
+        assert o['id'] == "OTHER"
+
+    def test_get_created(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.get_created() > 0
+
+    def test_exists(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.exists()
+        def sh_raises(cmd):
+            raise subprocess.CalledProcessError('FAIL', 'BAD')
+        with patch.multiple(
+                misc,
+                sh=sh_raises,
+        ):
+            o = OpenStackInstance('NAME')
+            assert not o.exists()
+
+    def test_volumes(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert len(o.get_volumes()) == 3
+
+    def test_get_addresses(self):
+        answers = [
+            self.teuthology_instance_no_addresses,
+            self.teuthology_instance,
+        ]
+        def sh(self):
+            return answers.pop(0)
+        with patch.multiple(
+                misc,
+                sh=sh,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.get_addresses() == 'Ext-Net=167.114.233.32'
+
+    def test_get_ip_neutron(self):
+        instance_id = '8e1fd70a-3065-46f8-9c30-84dc028c1834'
+        ip = '10.10.10.4'
+        def sh(cmd):
+            if 'neutron subnet-list' in cmd:
+                return """
+[
+  {
+    "ip_version": 6,
+    "id": "c45b9661-b2ba-4817-9e3a-f8f63bf32989"
+  },
+  {
+    "ip_version": 4,
+    "id": "e03a3dbc-afc8-4b52-952e-7bf755397b50"
+  }
+]
+                """
+            elif 'neutron port-list' in cmd:
+                return ("""
+[
+  {
+    "device_id": "915504ad-368b-4cce-be7c-4f8a83902e28",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.1\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc::1\\"}"
+  },
+  {
+    "device_id": "{instance_id}",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"{ip}\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe07:76c1\\"}"
+  },
+  {
+    "device_id": "17e4a968-4caa-4cee-8e4b-f950683a02bd",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.5\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe9c:37f0\\"}"
+  }
+]
+                """.replace('{instance_id}', instance_id).
+                        replace('{ip}', ip))
+            else:
+                raise Exception("unexpected " + cmd)
+        with patch.multiple(
+                misc,
+                sh=sh,
+        ):
+            assert ip == OpenStackInstance(
+                instance_id,
+                { 'id': instance_id },
+            ).get_ip_neutron()
+
 class TestOpenStack(object):
 
     def test_interpret_hints(self):
@@ -96,49 +206,6 @@ class TestOpenStack(object):
         else:
             del os.environ['OS_AUTH_URL']
 
-    def test_get_ip_neutron(self):
-        instance_id = '8e1fd70a-3065-46f8-9c30-84dc028c1834'
-        ip = '10.10.10.4'
-        def sh(cmd):
-            if 'neutron subnet-list' in cmd:
-                return """
-[
-  {
-    "ip_version": 6,
-    "id": "c45b9661-b2ba-4817-9e3a-f8f63bf32989"
-  },
-  {
-    "ip_version": 4,
-    "id": "e03a3dbc-afc8-4b52-952e-7bf755397b50"
-  }
-]
-                """
-            elif 'neutron port-list' in cmd:
-                return ("""
-[
-  {
-    "device_id": "915504ad-368b-4cce-be7c-4f8a83902e28",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.1\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc::1\\"}"
-  },
-  {
-    "device_id": "{instance_id}",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"{ip}\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe07:76c1\\"}"
-  },
-  {
-    "device_id": "17e4a968-4caa-4cee-8e4b-f950683a02bd",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.5\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe9c:37f0\\"}"
-  }
-]
-                """.replace('{instance_id}', instance_id).
-                        replace('{ip}', ip))
-            else:
-                raise Exception("unexpected " + cmd)
-        with patch.multiple(
-                misc,
-                sh=sh,
-        ):
-            assert ip == OpenStack.get_ip_neutron(instance_id)
-
 class TestTeuthologyOpenStack(object):
 
     @classmethod
index af61b5eefc1eb3483a0abc66adab80e01dbb451d..33fafa8ad470867107ab925384e6415dd9500951 100644 (file)
@@ -5,10 +5,11 @@ import os
 import random
 import re
 import subprocess
+import time
 import tempfile
 import yaml
 
-from .openstack import OpenStack
+from .openstack import OpenStack, OpenStackInstance
 from .config import config
 from .contextutil import safe_while
 from .misc import decanonicalize_hostname, get_distro, get_distro_version
@@ -266,23 +267,6 @@ class ProvisionOpenStack(OpenStack):
             misc.sh("openstack server add volume " +
                     name + " " + volume_name)
 
-    def list_volumes(self, name_or_id, server_info=None):
-        """
-        Return the uuid of the volumes attached to the name_or_id
-        OpenStack instance.
-
-        :param name_or_id:  The name or ID of the server to query
-        :param server_info: Optionally, use already-retrieved results of
-                            self.show()
-        """
-        if server_info is None:
-            server_info = self.show(name_or_id)
-        if not server_info:
-            return []
-        volumes = self.get_value(server_info,
-                                 'os-extended-volumes:volumes_attached')
-        return [volume['id'] for volume in volumes ]
-
     @staticmethod
     def ip2name(prefix, ip):
         """
@@ -325,22 +309,21 @@ class ProvisionOpenStack(OpenStack):
                 " --property ownedby=" + config.openstack['ip'] +
                 " --wait " +
                 " " + self.basename)
-        all_instances = json.loads(misc.sh("openstack server list -f json --long"))
         instances = filter(
             lambda instance: self.property in instance['Properties'],
-            all_instances)
+            self.list_instances())
+        instances = [OpenStackInstance(i['ID'], i) for i in instances]
         fqdns = []
         try:
             network = config['openstack'].get('network', '')
             for instance in instances:
-                name = self.ip2name(self.basename, self.get_ip(instance['ID'], network))
+                name = self.ip2name(self.basename, instance.get_ip(network))
                 misc.sh("openstack server set " +
                         "--name " + name + " " +
                         instance['ID'])
                 fqdn = name + '.' + config.lab_domain
                 if not misc.ssh_keyscan_wait(fqdn):
                     raise ValueError('ssh_keyscan_wait failed for ' + fqdn)
-                import time
                 time.sleep(15)
                 if not self.cloud_init_wait(fqdn):
                     raise ValueError('clound_init_wait failed for ' + fqdn)
@@ -354,22 +337,8 @@ class ProvisionOpenStack(OpenStack):
         return fqdns
 
     def destroy(self, name_or_id):
-        """
-        Delete the name_or_id OpenStack instance.
-        """
         log.debug('ProvisionOpenStack:destroy ' + name_or_id)
-        server_info = self.show(name_or_id)
-        if not self.exists(name_or_id, server_info=server_info):
-            return True
-        volumes = self.list_volumes(name_or_id, server_info=server_info)
-        server_id = self.get_value(server_info, 'ID')
-        misc.sh("openstack server set --name REMOVE-ME-" + name_or_id +
-                " " + server_id)
-        misc.sh("openstack server delete --wait " + server_id + " || true")
-        for volume in volumes:
-            misc.sh("openstack volume set --name REMOVE-ME " + volume)
-            misc.sh("openstack volume delete " + volume + " || true")
-        return True
+        return OpenStackInstance(name_or_id).destroy()
 
 
 def create_if_vm(ctx, machine_name, _downburst=None):