]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
Make teuthology.provision a subpackage
authorZack Cerza <zack@redhat.com>
Thu, 30 Jun 2016 15:58:11 +0000 (09:58 -0600)
committerZack Cerza <zack@redhat.com>
Thu, 30 Jun 2016 15:58:11 +0000 (09:58 -0600)
Signed-off-by: Zack Cerza <zack@redhat.com>
teuthology/provision.py [deleted file]
teuthology/provision/__init__.py [new file with mode: 0644]

diff --git a/teuthology/provision.py b/teuthology/provision.py
deleted file mode 100644 (file)
index 8b13c33..0000000
+++ /dev/null
@@ -1,437 +0,0 @@
-import json
-import logging
-import misc
-import os
-import random
-import re
-import subprocess
-import time
-import tempfile
-import yaml
-
-from subprocess import CalledProcessError
-
-from .openstack import OpenStack, OpenStackInstance
-from .config import config
-from .contextutil import safe_while
-from .exceptions import QuotaExceededError
-from .misc import decanonicalize_hostname, get_distro, get_distro_version
-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 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()
-
-
-class ProvisionOpenStack(OpenStack):
-    """
-    A class that provides methods for creating and destroying virtual machine
-    instances using OpenStack
-    """
-    def __init__(self):
-        super(ProvisionOpenStack, self).__init__()
-        self.user_data = tempfile.mktemp()
-        log.debug("ProvisionOpenStack: " + str(config.openstack))
-        self.basename = 'target'
-        self.up_string = 'The system is finally up'
-        self.property = "%16x" % random.getrandbits(128)
-
-    def __del__(self):
-        if os.path.exists(self.user_data):
-            os.unlink(self.user_data)
-
-    def init_user_data(self, os_type, os_version):
-        """
-        Get the user-data file that is fit for os_type and os_version.
-        It is responsible for setting up enough for ansible to take
-        over.
-        """
-        template_path = config['openstack']['user-data'].format(
-            os_type=os_type,
-            os_version=os_version)
-        nameserver = config['openstack'].get('nameserver', '8.8.8.8')
-        user_data_template = open(template_path).read()
-        user_data = user_data_template.format(
-            up=self.up_string,
-            nameserver=nameserver,
-            username=self.username,
-            lab_domain=config.lab_domain)
-        open(self.user_data, 'w').write(user_data)
-
-    def attach_volumes(self, name, volumes):
-        """
-        Create and attach volumes to the named OpenStack instance.
-        """
-        for i in range(volumes['count']):
-            volume_name = name + '-' + str(i)
-            try:
-                misc.sh("openstack volume show -f json " +
-                         volume_name)
-            except subprocess.CalledProcessError as e:
-                if 'No volume with a name or ID' not in e.output:
-                    raise e
-                misc.sh("openstack volume create -f json " +
-                        config['openstack'].get('volume-create', '') + " " +
-                        " --property ownedby=" + config.openstack['ip'] +
-                        " --size " + str(volumes['size']) + " " +
-                        volume_name)
-            with safe_while(sleep=2, tries=100,
-                            action="volume " + volume_name) as proceed:
-                while proceed():
-                    r = misc.sh("openstack volume show  -f json " +
-                                volume_name)
-                    status = self.get_value(json.loads(r), 'status')
-                    if status == 'available':
-                        break
-                    else:
-                        log.info("volume " + volume_name +
-                                 " not available yet")
-            misc.sh("openstack server add volume " +
-                    name + " " + volume_name)
-
-    @staticmethod
-    def ip2name(prefix, ip):
-        """
-        return the instance name suffixed with the /16 part of the IP.
-        """
-        digits = map(int, re.findall('.*\.(\d+)\.(\d+)', ip)[0])
-        return prefix + "%03d%03d" % tuple(digits)
-
-    def create(self, num, os_type, os_version, arch, resources_hint):
-        """
-        Create num OpenStack instances running os_type os_version and
-        return their names. Each instance has at least the resources
-        described in resources_hint.
-        """
-        log.debug('ProvisionOpenStack:create')
-        resources_hint = self.interpret_hints({
-            'machine': config['openstack']['machine'],
-            'volumes': config['openstack']['volumes'],
-        }, resources_hint)
-        self.init_user_data(os_type, os_version)
-        image = self.image(os_type, os_version)
-        if 'network' in config['openstack']:
-            net = "--nic net-id=" + str(self.net_id(config['openstack']['network']))
-        else:
-            net = ''
-        flavor = self.flavor(resources_hint['machine'],
-                             config['openstack'].get('flavor-select-regexp'))
-        cmd = ("flock --close --timeout 28800 /tmp/teuthology-server-create.lock" +
-               " openstack server create" +
-               " " + config['openstack'].get('server-create', '') +
-               " -f json " +
-               " --image '" + str(image) + "'" +
-               " --flavor '" + str(flavor) + "'" +
-               " --key-name teuthology " +
-               " --user-data " + str(self.user_data) +
-               " " + net +
-               " --min " + str(num) +
-               " --max " + str(num) +
-               " --security-group teuthology" +
-               " --property teuthology=" + self.property +
-               " --property ownedby=" + config.openstack['ip'] +
-               " --wait " +
-               " " + self.basename)
-        try:
-            misc.sh(cmd)
-        except CalledProcessError as exc:
-            if "quota exceeded" in exc.output.lower():
-                raise QuotaExceededError(message=exc.output)
-            raise
-        instances = filter(
-            lambda instance: self.property in instance['Properties'],
-            self.list_instances())
-        instances = [OpenStackInstance(i['ID']) for i in instances]
-        fqdns = []
-        try:
-            network = config['openstack'].get('network', '')
-            for instance in instances:
-                ip = instance.get_ip(network)
-                name = self.ip2name(self.basename, ip)
-                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)
-                time.sleep(15)
-                if not self.cloud_init_wait(instance):
-                    raise ValueError('cloud_init_wait failed for ' + fqdn)
-                self.attach_volumes(name, resources_hint['volumes'])
-                fqdns.append(fqdn)
-        except Exception as e:
-            log.exception(str(e))
-            for id in [instance['ID'] for instance in instances]:
-                self.destroy(id)
-            raise e
-        return fqdns
-
-    def destroy(self, name_or_id):
-        log.debug('ProvisionOpenStack:destroy ' + name_or_id)
-        return OpenStackInstance(name_or_id).destroy()
-
-
-def create_if_vm(ctx, machine_name, _downburst=None):
-    """
-    Use downburst to create a virtual machine
-
-    :param _downburst: Only used for unit testing.
-    """
-    if _downburst:
-        status_info = _downburst.status
-    else:
-        status_info = get_status(machine_name)
-    if not status_info.get('is_vm', False):
-        return False
-    os_type = get_distro(ctx)
-    os_version = get_distro_version(ctx)
-
-    has_config = hasattr(ctx, 'config') and ctx.config is not None
-    if has_config and 'downburst' in ctx.config:
-        log.warning(
-            'Usage of a custom downburst config has been deprecated.'
-        )
-
-    dbrst = _downburst or Downburst(name=machine_name, os_type=os_type,
-                                    os_version=os_version, status=status_info)
-    return dbrst.create()
-
-
-def destroy_if_vm(ctx, machine_name, user=None, description=None,
-                  _downburst=None):
-    """
-    Use downburst to destroy a virtual machine
-
-    Return False only on vm downburst failures.
-
-    :param _downburst: Only used for unit testing.
-    """
-    if _downburst:
-        status_info = _downburst.status
-    else:
-        status_info = get_status(machine_name)
-    if not status_info or not status_info.get('is_vm', False):
-        return True
-    if user is not None and user != status_info['locked_by']:
-        msg = "Tried to destroy {node} as {as_user} but it is locked " + \
-            "by {locked_by}"
-        log.error(msg.format(node=machine_name, as_user=user,
-                             locked_by=status_info['locked_by']))
-        return False
-    if (description is not None and description !=
-            status_info['description']):
-        msg = "Tried to destroy {node} with description {desc_arg} " + \
-            "but it is locked with description {desc_lock}"
-        log.error(msg.format(node=machine_name, desc_arg=description,
-                             desc_lock=status_info['description']))
-        return False
-    if status_info.get('machine_type') == 'openstack':
-        return ProvisionOpenStack().destroy(
-            decanonicalize_hostname(machine_name))
-
-    dbrst = _downburst or Downburst(name=machine_name, os_type=None,
-                                    os_version=None, status=status_info)
-    return dbrst.destroy()
diff --git a/teuthology/provision/__init__.py b/teuthology/provision/__init__.py
new file mode 100644 (file)
index 0000000..afedbe9
--- /dev/null
@@ -0,0 +1,438 @@
+import json
+import logging
+import os
+import random
+import re
+import subprocess
+import time
+import tempfile
+import yaml
+
+from subprocess import CalledProcessError
+
+from .. import misc
+
+from ..openstack import OpenStack, OpenStackInstance
+from ..config import config
+from ..contextutil import safe_while
+from ..exceptions import QuotaExceededError
+from ..misc import decanonicalize_hostname, get_distro, get_distro_version
+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 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()
+
+
+class ProvisionOpenStack(OpenStack):
+    """
+    A class that provides methods for creating and destroying virtual machine
+    instances using OpenStack
+    """
+    def __init__(self):
+        super(ProvisionOpenStack, self).__init__()
+        self.user_data = tempfile.mktemp()
+        log.debug("ProvisionOpenStack: " + str(config.openstack))
+        self.basename = 'target'
+        self.up_string = 'The system is finally up'
+        self.property = "%16x" % random.getrandbits(128)
+
+    def __del__(self):
+        if os.path.exists(self.user_data):
+            os.unlink(self.user_data)
+
+    def init_user_data(self, os_type, os_version):
+        """
+        Get the user-data file that is fit for os_type and os_version.
+        It is responsible for setting up enough for ansible to take
+        over.
+        """
+        template_path = config['openstack']['user-data'].format(
+            os_type=os_type,
+            os_version=os_version)
+        nameserver = config['openstack'].get('nameserver', '8.8.8.8')
+        user_data_template = open(template_path).read()
+        user_data = user_data_template.format(
+            up=self.up_string,
+            nameserver=nameserver,
+            username=self.username,
+            lab_domain=config.lab_domain)
+        open(self.user_data, 'w').write(user_data)
+
+    def attach_volumes(self, name, volumes):
+        """
+        Create and attach volumes to the named OpenStack instance.
+        """
+        for i in range(volumes['count']):
+            volume_name = name + '-' + str(i)
+            try:
+                misc.sh("openstack volume show -f json " +
+                         volume_name)
+            except subprocess.CalledProcessError as e:
+                if 'No volume with a name or ID' not in e.output:
+                    raise e
+                misc.sh("openstack volume create -f json " +
+                        config['openstack'].get('volume-create', '') + " " +
+                        " --property ownedby=" + config.openstack['ip'] +
+                        " --size " + str(volumes['size']) + " " +
+                        volume_name)
+            with safe_while(sleep=2, tries=100,
+                            action="volume " + volume_name) as proceed:
+                while proceed():
+                    r = misc.sh("openstack volume show  -f json " +
+                                volume_name)
+                    status = self.get_value(json.loads(r), 'status')
+                    if status == 'available':
+                        break
+                    else:
+                        log.info("volume " + volume_name +
+                                 " not available yet")
+            misc.sh("openstack server add volume " +
+                    name + " " + volume_name)
+
+    @staticmethod
+    def ip2name(prefix, ip):
+        """
+        return the instance name suffixed with the /16 part of the IP.
+        """
+        digits = map(int, re.findall('.*\.(\d+)\.(\d+)', ip)[0])
+        return prefix + "%03d%03d" % tuple(digits)
+
+    def create(self, num, os_type, os_version, arch, resources_hint):
+        """
+        Create num OpenStack instances running os_type os_version and
+        return their names. Each instance has at least the resources
+        described in resources_hint.
+        """
+        log.debug('ProvisionOpenStack:create')
+        resources_hint = self.interpret_hints({
+            'machine': config['openstack']['machine'],
+            'volumes': config['openstack']['volumes'],
+        }, resources_hint)
+        self.init_user_data(os_type, os_version)
+        image = self.image(os_type, os_version)
+        if 'network' in config['openstack']:
+            net = "--nic net-id=" + str(self.net_id(config['openstack']['network']))
+        else:
+            net = ''
+        flavor = self.flavor(resources_hint['machine'],
+                             config['openstack'].get('flavor-select-regexp'))
+        cmd = ("flock --close --timeout 28800 /tmp/teuthology-server-create.lock" +
+               " openstack server create" +
+               " " + config['openstack'].get('server-create', '') +
+               " -f json " +
+               " --image '" + str(image) + "'" +
+               " --flavor '" + str(flavor) + "'" +
+               " --key-name teuthology " +
+               " --user-data " + str(self.user_data) +
+               " " + net +
+               " --min " + str(num) +
+               " --max " + str(num) +
+               " --security-group teuthology" +
+               " --property teuthology=" + self.property +
+               " --property ownedby=" + config.openstack['ip'] +
+               " --wait " +
+               " " + self.basename)
+        try:
+            misc.sh(cmd)
+        except CalledProcessError as exc:
+            if "quota exceeded" in exc.output.lower():
+                raise QuotaExceededError(message=exc.output)
+            raise
+        instances = filter(
+            lambda instance: self.property in instance['Properties'],
+            self.list_instances())
+        instances = [OpenStackInstance(i['ID']) for i in instances]
+        fqdns = []
+        try:
+            network = config['openstack'].get('network', '')
+            for instance in instances:
+                ip = instance.get_ip(network)
+                name = self.ip2name(self.basename, ip)
+                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)
+                time.sleep(15)
+                if not self.cloud_init_wait(instance):
+                    raise ValueError('cloud_init_wait failed for ' + fqdn)
+                self.attach_volumes(name, resources_hint['volumes'])
+                fqdns.append(fqdn)
+        except Exception as e:
+            log.exception(str(e))
+            for id in [instance['ID'] for instance in instances]:
+                self.destroy(id)
+            raise e
+        return fqdns
+
+    def destroy(self, name_or_id):
+        log.debug('ProvisionOpenStack:destroy ' + name_or_id)
+        return OpenStackInstance(name_or_id).destroy()
+
+
+def create_if_vm(ctx, machine_name, _downburst=None):
+    """
+    Use downburst to create a virtual machine
+
+    :param _downburst: Only used for unit testing.
+    """
+    if _downburst:
+        status_info = _downburst.status
+    else:
+        status_info = get_status(machine_name)
+    if not status_info.get('is_vm', False):
+        return False
+    os_type = get_distro(ctx)
+    os_version = get_distro_version(ctx)
+
+    has_config = hasattr(ctx, 'config') and ctx.config is not None
+    if has_config and 'downburst' in ctx.config:
+        log.warning(
+            'Usage of a custom downburst config has been deprecated.'
+        )
+
+    dbrst = _downburst or Downburst(name=machine_name, os_type=os_type,
+                                    os_version=os_version, status=status_info)
+    return dbrst.create()
+
+
+def destroy_if_vm(ctx, machine_name, user=None, description=None,
+                  _downburst=None):
+    """
+    Use downburst to destroy a virtual machine
+
+    Return False only on vm downburst failures.
+
+    :param _downburst: Only used for unit testing.
+    """
+    if _downburst:
+        status_info = _downburst.status
+    else:
+        status_info = get_status(machine_name)
+    if not status_info or not status_info.get('is_vm', False):
+        return True
+    if user is not None and user != status_info['locked_by']:
+        msg = "Tried to destroy {node} as {as_user} but it is locked " + \
+            "by {locked_by}"
+        log.error(msg.format(node=machine_name, as_user=user,
+                             locked_by=status_info['locked_by']))
+        return False
+    if (description is not None and description !=
+            status_info['description']):
+        msg = "Tried to destroy {node} with description {desc_arg} " + \
+            "but it is locked with description {desc_lock}"
+        log.error(msg.format(node=machine_name, desc_arg=description,
+                             desc_lock=status_info['description']))
+        return False
+    if status_info.get('machine_type') == 'openstack':
+        return ProvisionOpenStack().destroy(
+            decanonicalize_hostname(machine_name))
+
+    dbrst = _downburst or Downburst(name=machine_name, os_type=None,
+                                    os_version=None, status=status_info)
+    return dbrst.destroy()