From: Zack Cerza Date: Thu, 30 Jun 2016 15:59:09 +0000 (-0600) Subject: Move downburst-specific code to its own module X-Git-Tag: 1.1.0~579^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=ca9e2572ba90183c67a4da18487537386bdf84af;p=teuthology.git Move downburst-specific code to its own module Signed-off-by: Zack Cerza --- diff --git a/teuthology/lock.py b/teuthology/lock.py index 9ed3f53a1..bc6458c42 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 afedbe93d..781863472 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 000000000..b42ae4cfa --- /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/provision/test/test_downburst.py b/teuthology/provision/test/test_downburst.py new file mode 100644 index 000000000..ecbf400b1 --- /dev/null +++ b/teuthology/provision/test/test_downburst.py @@ -0,0 +1,98 @@ +from mock import Mock, MagicMock, patch + +from teuthology import provision + + +class TestDownburst(object): + def setup(self): + self.ctx = Mock() + self.ctx.os_type = 'rhel' + self.ctx.os_version = '7.0' + self.ctx.config = dict() + self.name = 'vpm999' + self.status = dict( + vm_host=dict(name='host999'), + is_vm=True, + ) + + def test_create_if_vm_success(self): + name = self.name + ctx = self.ctx + status = self.status + + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + dbrst.executable = '/fake/path' + dbrst.build_config = MagicMock(name='build_config') + dbrst._run_create = MagicMock(name='_run_create') + dbrst._run_create.return_value = (0, '', '') + remove_config = MagicMock(name='remove_config') + dbrst.remove_config = remove_config + + result = provision.create_if_vm(ctx, name, dbrst) + assert result is True + + dbrst._run_create.assert_called_with() + dbrst.build_config.assert_called_with() + del dbrst + remove_config.assert_called_with() + + def test_destroy_if_vm_success(self): + name = self.name + ctx = self.ctx + status = self.status + + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + dbrst.destroy = MagicMock(name='destroy') + dbrst.destroy.return_value = True + + result = provision.destroy_if_vm(ctx, name, _downburst=dbrst) + assert result is True + + dbrst.destroy.assert_called_with() + + def test_destroy_if_vm_wrong_owner(self): + name = self.name + ctx = self.ctx + status = self.status + status['locked_by'] = 'user@a' + + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError) + + result = provision.destroy_if_vm(ctx, name, user='user@b', + _downburst=dbrst) + assert result is False + + def test_destroy_if_vm_wrong_description(self): + name = self.name + ctx = self.ctx + status = self.status + status['description'] = 'desc_a' + + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + dbrst.destroy = MagicMock(name='destroy') + dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError) + + result = provision.destroy_if_vm(ctx, name, description='desc_b', + _downburst=dbrst) + assert result is False + + @patch('teuthology.provision.downburst.downburst_executable') + def test_create_fails_without_executable(self, m_exec): + name = self.name + ctx = self.ctx + status = self.status + m_exec.return_value = '' + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + result = dbrst.create() + assert result is False + + @patch('teuthology.provision.downburst.downburst_executable') + def test_destroy_fails_without_executable(self, m_exec): + name = self.name + ctx = self.ctx + status = self.status + m_exec.return_value = '' + dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) + result = dbrst.destroy() + assert result is False diff --git a/teuthology/test/test_provision.py b/teuthology/test/test_provision.py deleted file mode 100644 index 29259769f..000000000 --- a/teuthology/test/test_provision.py +++ /dev/null @@ -1,98 +0,0 @@ -from mock import Mock, MagicMock, patch - -from teuthology import provision - - -class TestDownburst(object): - def setup(self): - self.ctx = Mock() - self.ctx.os_type = 'rhel' - self.ctx.os_version = '7.0' - self.ctx.config = dict() - self.name = 'vpm999' - self.status = dict( - vm_host=dict(name='host999'), - is_vm=True, - ) - - def test_create_if_vm_success(self): - name = self.name - ctx = self.ctx - status = self.status - - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - dbrst.executable = '/fake/path' - dbrst.build_config = MagicMock(name='build_config') - dbrst._run_create = MagicMock(name='_run_create') - dbrst._run_create.return_value = (0, '', '') - remove_config = MagicMock(name='remove_config') - dbrst.remove_config = remove_config - - result = provision.create_if_vm(ctx, name, dbrst) - assert result is True - - dbrst._run_create.assert_called_with() - dbrst.build_config.assert_called_with() - del dbrst - remove_config.assert_called_with() - - def test_destroy_if_vm_success(self): - name = self.name - ctx = self.ctx - status = self.status - - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - dbrst.destroy = MagicMock(name='destroy') - dbrst.destroy.return_value = True - - result = provision.destroy_if_vm(ctx, name, _downburst=dbrst) - assert result is True - - dbrst.destroy.assert_called_with() - - def test_destroy_if_vm_wrong_owner(self): - name = self.name - ctx = self.ctx - status = self.status - status['locked_by'] = 'user@a' - - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError) - - result = provision.destroy_if_vm(ctx, name, user='user@b', - _downburst=dbrst) - assert result is False - - def test_destroy_if_vm_wrong_description(self): - name = self.name - ctx = self.ctx - status = self.status - status['description'] = 'desc_a' - - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - dbrst.destroy = MagicMock(name='destroy') - dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError) - - result = provision.destroy_if_vm(ctx, name, description='desc_b', - _downburst=dbrst) - assert result is False - - @patch('teuthology.provision.downburst_executable') - def test_create_fails_without_executable(self, m_exec): - name = self.name - ctx = self.ctx - status = self.status - m_exec.return_value = '' - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - result = dbrst.create() - assert result is False - - @patch('teuthology.provision.downburst_executable') - def test_destroy_fails_without_executable(self, m_exec): - name = self.name - ctx = self.ctx - status = self.status - m_exec.return_value = '' - dbrst = provision.Downburst(name, ctx.os_type, ctx.os_version, status) - result = dbrst.destroy() - assert result is False diff --git a/teuthology/test/test_vps_os_vers_parameter_checking.py b/teuthology/test/test_vps_os_vers_parameter_checking.py index 893f87333..9e9fa0219 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(