From 76d1472af1b4da4afb14f3521ddaf0c9d07dd7a0 Mon Sep 17 00:00:00 2001 From: Loic Dachary Date: Sun, 22 Nov 2015 23:57:30 +0100 Subject: [PATCH] openstack: introduce OpenStackInstance To reduce the number of redundant calls and reduce the code complexity. Signed-off-by: Loic Dachary --- teuthology/openstack/__init__.py | 221 ++++++++++++-------- teuthology/openstack/test/test_openstack.py | 155 ++++++++++---- teuthology/provision.py | 43 +--- 3 files changed, 246 insertions(+), 173 deletions(-) diff --git a/teuthology/openstack/__init__.py b/teuthology/openstack/__init__.py index 36ab682650..8ed0b0b3ea 100644 --- a/teuthology/openstack/__init__.py +++ b/teuthology/openstack/__init__.py @@ -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 " 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): diff --git a/teuthology/openstack/test/test_openstack.py b/teuthology/openstack/test/test_openstack.py index feebaa0c84..32fecb5b72 100644 --- a/teuthology/openstack/test/test_openstack.py +++ b/teuthology/openstack/test/test_openstack.py @@ -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 diff --git a/teuthology/provision.py b/teuthology/provision.py index af61b5eefc..33fafa8ad4 100644 --- a/teuthology/provision.py +++ b/teuthology/provision.py @@ -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): -- 2.39.5