From ca9e2572ba90183c67a4da18487537386bdf84af Mon Sep 17 00:00:00 2001 From: Zack Cerza Date: Thu, 30 Jun 2016 09:59:09 -0600 Subject: [PATCH] Move downburst-specific code to its own module Signed-off-by: Zack Cerza --- teuthology/lock.py | 2 +- teuthology/provision/__init__.py | 207 +---------------- teuthology/provision/downburst.py | 217 ++++++++++++++++++ .../test/test_downburst.py} | 4 +- .../test_vps_os_vers_parameter_checking.py | 13 +- 5 files changed, 228 insertions(+), 215 deletions(-) create mode 100644 teuthology/provision/downburst.py rename teuthology/{test/test_provision.py => provision/test/test_downburst.py} (96%) diff --git a/teuthology/lock.py b/teuthology/lock.py index 9ed3f53a1a..bc6458c42f 100644 --- a/teuthology/lock.py +++ b/teuthology/lock.py @@ -47,7 +47,7 @@ def get_distro_from_downburst(): u'16.04(xenial)'], u'sles': [u'11-sp2'], u'debian': [u'6.0', u'7.0', u'8.0']} - executable_cmd = provision.downburst_executable() + executable_cmd = provision.downburst.downburst_executable() if not executable_cmd: log.warn("Downburst not found!") log.info('Using default values for supported os_type/os_version') diff --git a/teuthology/provision/__init__.py b/teuthology/provision/__init__.py index afedbe93df..781863472e 100644 --- a/teuthology/provision/__init__.py +++ b/teuthology/provision/__init__.py @@ -6,7 +6,6 @@ import re import subprocess import time import tempfile -import yaml from subprocess import CalledProcessError @@ -19,212 +18,10 @@ from ..exceptions import QuotaExceededError from ..misc import decanonicalize_hostname, get_distro, get_distro_version from ..lockstatus import get_status +from .downburst import Downburst -log = logging.getLogger(__name__) - - -def downburst_executable(): - """ - First check for downburst in the user's path. - Then check in ~/src, ~ubuntu/src, and ~teuthology/src. - Return '' if no executable downburst is found. - """ - if config.downburst: - return config.downburst - path = os.environ.get('PATH', None) - if path: - for p in os.environ.get('PATH', '').split(os.pathsep): - pth = os.path.join(p, 'downburst') - if os.access(pth, os.X_OK): - return pth - import pwd - little_old_me = pwd.getpwuid(os.getuid()).pw_name - for user in [little_old_me, 'ubuntu', 'teuthology']: - pth = os.path.expanduser( - "~%s/src/downburst/virtualenv/bin/downburst" % user) - if os.access(pth, os.X_OK): - return pth - return '' - - -class Downburst(object): - """ - A class that provides methods for creating and destroying virtual machine - instances using downburst: https://github.com/ceph/downburst - """ - def __init__(self, name, os_type, os_version, status=None, user='ubuntu'): - self.name = name - self.os_type = os_type - self.os_version = os_version - self.status = status or get_status(self.name) - self.config_path = None - self.user_path = None - self.user = user - self.host = decanonicalize_hostname(self.status['vm_host']['name']) - self.executable = downburst_executable() - - def create(self): - """ - Launch a virtual machine instance. - - If creation fails because an instance with the specified name is - already running, first destroy it, then try again. This process will - repeat two more times, waiting 60s between tries, before giving up. - """ - if not self.executable: - log.error("No downburst executable found.") - return False - self.build_config() - success = None - with safe_while(sleep=60, tries=3, - action="downburst create") as proceed: - while proceed(): - (returncode, stdout, stderr) = self._run_create() - if returncode == 0: - log.info("Downburst created %s: %s" % (self.name, - stdout.strip())) - success = True - break - elif stderr: - # If the guest already exists first destroy then re-create: - if 'exists' in stderr: - success = False - log.info("Guest files exist. Re-creating guest: %s" % - (self.name)) - self.destroy() - else: - success = False - log.info("Downburst failed on %s: %s" % ( - self.name, stderr.strip())) - break - return success - - def _run_create(self): - """ - Used by create(), this method is what actually calls downburst when - creating a virtual machine instance. - """ - if not self.config_path: - raise ValueError("I need a config_path!") - if not self.user_path: - raise ValueError("I need a user_path!") - shortname = decanonicalize_hostname(self.name) - args = [ - self.executable, - '-c', self.host, - 'create', - '--wait', - '--meta-data=%s' % self.config_path, - '--user-data=%s' % self.user_path, - shortname, - ] - log.info("Provisioning a {distro} {distroversion} vps".format( - distro=self.os_type, - distroversion=self.os_version - )) - proc = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = proc.communicate() - return (proc.returncode, out, err) - - def destroy(self): - """ - Destroy (shutdown and delete) a virtual machine instance. - """ - executable = self.executable - if not executable: - log.error("No downburst executable found.") - return False - shortname = decanonicalize_hostname(self.name) - args = [executable, '-c', self.host, 'destroy', shortname] - proc = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE,) - out, err = proc.communicate() - if err: - log.error("Error destroying {machine}: {msg}".format( - machine=self.name, msg=err)) - return False - elif proc.returncode == 0: - out_str = ': %s' % out if out else '' - log.info("Destroyed %s%s" % (self.name, out_str)) - return True - else: - log.error("I don't know if the destroy of {node} succeded!".format( - node=self.name)) - return False - - def build_config(self): - """ - Assemble a configuration to pass to downburst, and write it to a file. - """ - config_fd = tempfile.NamedTemporaryFile(delete=False) - - os_type = self.os_type.lower() - mac_address = self.status['mac_address'] - - file_info = { - 'disk-size': '100G', - 'ram': '3.8G', - 'cpus': 1, - 'networks': [ - {'source': 'front', 'mac': mac_address}], - 'distro': os_type, - 'distroversion': self.os_version, - 'additional-disks': 3, - 'additional-disks-size': '200G', - 'arch': 'x86_64', - } - fqdn = self.name.split('@')[1] - file_out = { - 'downburst': file_info, - 'local-hostname': fqdn, - } - yaml.safe_dump(file_out, config_fd) - self.config_path = config_fd.name - - user_info = { - 'user': self.user, - # Remove the user's password so console logins are possible - 'runcmd': [ - ['passwd', '-d', self.user], - ] - } - # On CentOS/RHEL/Fedora, write the correct mac address - if os_type in ['centos', 'rhel', 'fedora']: - user_info['runcmd'].extend([ - ['sed', '-ie', 's/HWADDR=".*"/HWADDR="%s"/' % mac_address, - '/etc/sysconfig/network-scripts/ifcfg-eth0'], - ]) - # On Ubuntu, starting with 16.04, we need to install 'python' to get - # python2.7, which ansible needs - elif os_type == 'ubuntu': - if not 'packages' in user_info: - user_info['packages'] = list() - user_info['packages'].extend([ - 'python', - ]) - user_fd = tempfile.NamedTemporaryFile(delete=False) - yaml.safe_dump(user_info, user_fd) - self.user_path = user_fd.name - return True - - def remove_config(self): - """ - Remove the downburst configuration file created by build_config() - """ - if self.config_path and os.path.exists(self.config_path): - os.remove(self.config_path) - self.config_path = None - return True - if self.user_path and os.path.exists(self.user_path): - os.remove(self.user_path) - self.user_path = None - return True - return False - - def __del__(self): - self.remove_config() +log = logging.getLogger(__name__) class ProvisionOpenStack(OpenStack): diff --git a/teuthology/provision/downburst.py b/teuthology/provision/downburst.py new file mode 100644 index 0000000000..b42ae4cfae --- /dev/null +++ b/teuthology/provision/downburst.py @@ -0,0 +1,217 @@ +import logging +import os +import subprocess +import tempfile +import yaml + +from ..config import config +from ..contextutil import safe_while +from ..misc import decanonicalize_hostname +from ..lockstatus import get_status + + +log = logging.getLogger(__name__) + + +def downburst_executable(): + """ + First check for downburst in the user's path. + Then check in ~/src, ~ubuntu/src, and ~teuthology/src. + Return '' if no executable downburst is found. + """ + if config.downburst: + return config.downburst + path = os.environ.get('PATH', None) + if path: + for p in os.environ.get('PATH', '').split(os.pathsep): + pth = os.path.join(p, 'downburst') + if os.access(pth, os.X_OK): + return pth + import pwd + little_old_me = pwd.getpwuid(os.getuid()).pw_name + for user in [little_old_me, 'ubuntu', 'teuthology']: + pth = os.path.expanduser( + "~%s/src/downburst/virtualenv/bin/downburst" % user) + if os.access(pth, os.X_OK): + return pth + return '' + + +class Downburst(object): + """ + A class that provides methods for creating and destroying virtual machine + instances using downburst: https://github.com/ceph/downburst + """ + def __init__(self, name, os_type, os_version, status=None, user='ubuntu'): + self.name = name + self.os_type = os_type + self.os_version = os_version + self.status = status or get_status(self.name) + self.config_path = None + self.user_path = None + self.user = user + self.host = decanonicalize_hostname(self.status['vm_host']['name']) + self.executable = downburst_executable() + + def create(self): + """ + Launch a virtual machine instance. + + If creation fails because an instance with the specified name is + already running, first destroy it, then try again. This process will + repeat two more times, waiting 60s between tries, before giving up. + """ + if not self.executable: + log.error("No downburst executable found.") + return False + self.build_config() + success = None + with safe_while(sleep=60, tries=3, + action="downburst create") as proceed: + while proceed(): + (returncode, stdout, stderr) = self._run_create() + if returncode == 0: + log.info("Downburst created %s: %s" % (self.name, + stdout.strip())) + success = True + break + elif stderr: + # If the guest already exists first destroy then re-create: + if 'exists' in stderr: + success = False + log.info("Guest files exist. Re-creating guest: %s" % + (self.name)) + self.destroy() + else: + success = False + log.info("Downburst failed on %s: %s" % ( + self.name, stderr.strip())) + break + return success + + def _run_create(self): + """ + Used by create(), this method is what actually calls downburst when + creating a virtual machine instance. + """ + if not self.config_path: + raise ValueError("I need a config_path!") + if not self.user_path: + raise ValueError("I need a user_path!") + shortname = decanonicalize_hostname(self.name) + + args = [ + self.executable, + '-c', self.host, + 'create', + '--wait', + '--meta-data=%s' % self.config_path, + '--user-data=%s' % self.user_path, + shortname, + ] + log.info("Provisioning a {distro} {distroversion} vps".format( + distro=self.os_type, + distroversion=self.os_version + )) + proc = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = proc.communicate() + return (proc.returncode, out, err) + + def destroy(self): + """ + Destroy (shutdown and delete) a virtual machine instance. + """ + executable = self.executable + if not executable: + log.error("No downburst executable found.") + return False + shortname = decanonicalize_hostname(self.name) + args = [executable, '-c', self.host, 'destroy', shortname] + proc = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE,) + out, err = proc.communicate() + if err: + log.error("Error destroying {machine}: {msg}".format( + machine=self.name, msg=err)) + return False + elif proc.returncode == 0: + out_str = ': %s' % out if out else '' + log.info("Destroyed %s%s" % (self.name, out_str)) + return True + else: + log.error("I don't know if the destroy of {node} succeded!".format( + node=self.name)) + return False + + def build_config(self): + """ + Assemble a configuration to pass to downburst, and write it to a file. + """ + config_fd = tempfile.NamedTemporaryFile(delete=False) + + os_type = self.os_type.lower() + mac_address = self.status['mac_address'] + + file_info = { + 'disk-size': '100G', + 'ram': '3.8G', + 'cpus': 1, + 'networks': [ + {'source': 'front', 'mac': mac_address}], + 'distro': os_type, + 'distroversion': self.os_version, + 'additional-disks': 3, + 'additional-disks-size': '200G', + 'arch': 'x86_64', + } + fqdn = self.name.split('@')[1] + file_out = { + 'downburst': file_info, + 'local-hostname': fqdn, + } + yaml.safe_dump(file_out, config_fd) + self.config_path = config_fd.name + + user_info = { + 'user': self.user, + # Remove the user's password so console logins are possible + 'runcmd': [ + ['passwd', '-d', self.user], + ] + } + # On CentOS/RHEL/Fedora, write the correct mac address + if os_type in ['centos', 'rhel', 'fedora']: + user_info['runcmd'].extend([ + ['sed', '-ie', 's/HWADDR=".*"/HWADDR="%s"/' % mac_address, + '/etc/sysconfig/network-scripts/ifcfg-eth0'], + ]) + # On Ubuntu, starting with 16.04, we need to install 'python' to get + # python2.7, which ansible needs + elif os_type == 'ubuntu': + if 'packages' not in user_info: + user_info['packages'] = list() + user_info['packages'].extend([ + 'python', + ]) + user_fd = tempfile.NamedTemporaryFile(delete=False) + yaml.safe_dump(user_info, user_fd) + self.user_path = user_fd.name + return True + + def remove_config(self): + """ + Remove the downburst configuration file created by build_config() + """ + if self.config_path and os.path.exists(self.config_path): + os.remove(self.config_path) + self.config_path = None + return True + if self.user_path and os.path.exists(self.user_path): + os.remove(self.user_path) + self.user_path = None + return True + return False + + def __del__(self): + self.remove_config() diff --git a/teuthology/test/test_provision.py b/teuthology/provision/test/test_downburst.py similarity index 96% rename from teuthology/test/test_provision.py rename to teuthology/provision/test/test_downburst.py index 29259769f7..ecbf400b10 100644 --- a/teuthology/test/test_provision.py +++ b/teuthology/provision/test/test_downburst.py @@ -77,7 +77,7 @@ class TestDownburst(object): _downburst=dbrst) assert result is False - @patch('teuthology.provision.downburst_executable') + @patch('teuthology.provision.downburst.downburst_executable') def test_create_fails_without_executable(self, m_exec): name = self.name ctx = self.ctx @@ -87,7 +87,7 @@ class TestDownburst(object): result = dbrst.create() assert result is False - @patch('teuthology.provision.downburst_executable') + @patch('teuthology.provision.downburst.downburst_executable') def test_destroy_fails_without_executable(self, m_exec): name = self.name ctx = self.ctx diff --git a/teuthology/test/test_vps_os_vers_parameter_checking.py b/teuthology/test/test_vps_os_vers_parameter_checking.py index 893f87333f..9e9fa02190 100644 --- a/teuthology/test/test_vps_os_vers_parameter_checking.py +++ b/teuthology/test/test_vps_os_vers_parameter_checking.py @@ -1,8 +1,7 @@ -from mock import patch +from mock import patch, Mock from .. import lock -class Mock: pass class TestVpsOsVersionParamCheck(object): @@ -21,7 +20,7 @@ class TestVpsOsVersionParamCheck(object): self.fake_ctx.os_type = 'ubuntu' self.fake_ctx.os_version = 'precise' with patch.multiple( - lock.provision, + lock.provision.downburst, downburst_executable=self.fake_downburst_executable, ): check_value = lock.vps_version_or_type_valid( @@ -35,7 +34,7 @@ class TestVpsOsVersionParamCheck(object): self.fake_ctx.os_type = 'ubuntu' self.fake_ctx.os_version = '12.04' with patch.multiple( - lock.provision, + lock.provision.downburst, downburst_executable=self.fake_downburst_executable, ): check_value = lock.vps_version_or_type_valid( @@ -48,7 +47,7 @@ class TestVpsOsVersionParamCheck(object): self.fake_ctx.os_type = '6.5' self.fake_ctx.os_version = 'rhel' with patch.multiple( - lock.provision, + lock.provision.downburst, downburst_executable=self.fake_downburst_executable, ): check_value = lock.vps_version_or_type_valid( @@ -61,7 +60,7 @@ class TestVpsOsVersionParamCheck(object): self.fake_ctx.os_type = 'aardvark' self.fake_ctx.os_version = '6.5' with patch.multiple( - lock.provision, + lock.provision.downburst, downburst_executable=self.fake_downburst_executable, ): check_value = lock.vps_version_or_type_valid( @@ -74,7 +73,7 @@ class TestVpsOsVersionParamCheck(object): self.fake_ctx.os_type = 'rhel' self.fake_ctx.os_version = 'vampire_bat' with patch.multiple( - lock.provision, + lock.provision.downburst, downburst_executable=self.fake_downburst_executable, ): check_value = lock.vps_version_or_type_valid( -- 2.39.5