chiark / gitweb /
vmtools: use standard imports: os.path.join()
[fdroidserver.git] / fdroidserver / vmtools.py
index e502f3d345e122a5db2dccaa14ded8be24a4bba3..f6aa0a8bb568252170bd1eee3e18df1df4dcab50 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from os import remove as rmfile
-from os.path import isdir, isfile, join as joinpath, basename, abspath, expanduser
+from os.path import isdir, isfile, basename, abspath, expanduser
+import os
 import math
 import json
 import tarfile
-import time
 import shutil
 import subprocess
 import textwrap
 from .common import FDroidException
 from logging import getLogger
 
+from fdroidserver import _
+import threading
+
+lock = threading.Lock()
+
 logger = getLogger('fdroidserver-vmtools')
 
 
-def _check_call(cmd, shell=False, cwd=None):
+def get_clean_builder(serverdir, reset=False):
+    if not os.path.isdir(serverdir):
+        if os.path.islink(serverdir):
+            os.unlink(serverdir)
+        logger.info("buildserver path does not exists, creating %s", serverdir)
+        os.makedirs(serverdir)
+    vagrantfile = os.path.join(serverdir, 'Vagrantfile')
+    if not os.path.isfile(vagrantfile):
+        with open(os.path.join('builder', 'Vagrantfile'), 'w') as f:
+            f.write(textwrap.dedent("""\
+                # generated file, do not change.
+
+                Vagrant.configure("2") do |config|
+                    config.vm.box = "buildserver"
+                    config.vm.synced_folder ".", "/vagrant", disabled: true
+                end
+                """))
+    vm = get_build_vm(serverdir)
+    if reset:
+        logger.info('resetting buildserver by request')
+    elif not vm.vagrant_uuid_okay():
+        logger.info('resetting buildserver, because vagrant vm is not okay.')
+        reset = True
+    elif not vm.snapshot_exists('fdroidclean'):
+        logger.info("resetting buildserver, because snapshot 'fdroidclean' is not present.")
+        reset = True
+
+    if reset:
+        vm.destroy()
+    vm.up()
+    vm.suspend()
+
+    if reset:
+        logger.info('buildserver recreated: taking a clean snapshot')
+        vm.snapshot_create('fdroidclean')
+    else:
+        logger.info('builserver ok: reverting to clean snapshot')
+        vm.snapshot_revert('fdroidclean')
+    vm.up()
+
+    try:
+        sshinfo = vm.sshinfo()
+    except FDroidBuildVmException:
+        # workaround because libvirt sometimes likes to forget
+        # about ssh connection info even thou the vm is running
+        vm.halt()
+        vm.up()
+        sshinfo = vm.sshinfo()
+
+    return sshinfo
+
+
+def _check_call(cmd, cwd=None):
     logger.debug(' '.join(cmd))
-    return subprocess.check_call(cmd, shell=shell, cwd=cwd)
+    return subprocess.check_call(cmd, shell=False, cwd=cwd)
 
 
-def _check_output(cmd, shell=False, cwd=None):
+def _check_output(cmd, cwd=None):
     logger.debug(' '.join(cmd))
-    return subprocess.check_output(cmd, shell=shell, cwd=cwd)
+    return subprocess.check_output(cmd, shell=False, cwd=cwd)
 
 
 def get_build_vm(srvdir, provider=None):
@@ -90,10 +147,10 @@ def get_build_vm(srvdir, provider=None):
         logger.debug('could not confirm that either virtualbox or kvm/libvirt are installed')
 
     # try guessing provider from .../srvdir/.vagrant internals
-    has_libvirt_machine = isdir(joinpath(abssrvdir, '.vagrant',
-                                         'machines', 'default', 'libvirt'))
-    has_vbox_machine = isdir(joinpath(abssrvdir, '.vagrant',
-                                      'machines', 'default', 'libvirt'))
+    has_libvirt_machine = isdir(os.path.join(abssrvdir, '.vagrant',
+                                             'machines', 'default', 'libvirt'))
+    has_vbox_machine = isdir(os.path.join(abssrvdir, '.vagrant',
+                                          'machines', 'default', 'virtualbox'))
     if has_libvirt_machine and has_vbox_machine:
         logger.info('build vm provider lookup found virtualbox and libvirt, defaulting to \'virtualbox\'')
         return VirtualboxBuildVm(abssrvdir)
@@ -121,13 +178,12 @@ class FDroidBuildVm():
     This is intended to be a hypervisor independant, fault tolerant
     wrapper around the vagrant functions we use.
     """
-
     def __init__(self, srvdir):
         """Create new server class.
         """
         self.srvdir = srvdir
         self.srvname = basename(srvdir) + '_default'
-        self.vgrntfile = joinpath(srvdir, 'Vagrantfile')
+        self.vgrntfile = os.path.join(srvdir, 'Vagrantfile')
         self.srvuuid = self._vagrant_fetch_uuid()
         if not isdir(srvdir):
             raise FDroidBuildVmException("Can not init vagrant, directory %s not present" % (srvdir))
@@ -137,25 +193,27 @@ class FDroidBuildVm():
         self.vgrnt = vagrant.Vagrant(root=srvdir, out_cm=vagrant.stdout_cm, err_cm=vagrant.stdout_cm)
 
     def up(self, provision=True):
-        try:
-            self.vgrnt.up(provision=provision)
-            logger.info('...waiting a sec...')
-            time.sleep(10)
-            self.srvuuid = self._vagrant_fetch_uuid()
-        except subprocess.CalledProcessError as e:
-            raise FDroidBuildVmException("could not bring up vm '%s'" % self.srvname) from e
+        global lock
+        with lock:
+            try:
+                self.vgrnt.up(provision=provision)
+                self.srvuuid = self._vagrant_fetch_uuid()
+            except subprocess.CalledProcessError as e:
+                raise FDroidBuildVmException("could not bring up vm '%s'" % self.srvname) from e
 
     def suspend(self):
-        logger.info('suspending buildserver')
-        try:
-            self.vgrnt.suspend()
-            logger.info('...waiting a sec...')
-            time.sleep(10)
-        except subprocess.CalledProcessError as e:
-            raise FDroidBuildVmException("could not suspend vm '%s'" % self.srvname) from e
+        global lock
+        with lock:
+            logger.info('suspending buildserver')
+            try:
+                self.vgrnt.suspend()
+            except subprocess.CalledProcessError as e:
+                raise FDroidBuildVmException("could not suspend vm '%s'" % self.srvname) from e
 
     def halt(self):
-        self.vgrnt.halt(force=True)
+        global lock
+        with lock:
+            self.vgrnt.halt(force=True)
 
     def destroy(self):
         """Remove every trace of this VM from the system.
@@ -171,7 +229,7 @@ class FDroidBuildVm():
             logger.debug('vagrant destroy completed')
         except subprocess.CalledProcessError as e:
             logger.exception('vagrant destroy failed: %s', e)
-        vgrntdir = joinpath(self.srvdir, '.vagrant')
+        vgrntdir = os.path.join(self.srvdir, '.vagrant')
         try:
             shutil.rmtree(vgrntdir)
             logger.debug('deleted vagrant dir: %s', vgrntdir)
@@ -195,17 +253,17 @@ class FDroidBuildVm():
         return name.replace('/', '-VAGRANTSLASH-')
 
     def _vagrant_fetch_uuid(self):
-        if isfile(joinpath(self.srvdir, '.vagrant')):
+        if isfile(os.path.join(self.srvdir, '.vagrant')):
             # Vagrant 1.0 - it's a json file...
-            with open(joinpath(self.srvdir, '.vagrant')) as f:
+            with open(os.path.join(self.srvdir, '.vagrant')) as f:
                 id = json.load(f)['active']['default']
                 logger.debug('vm uuid: %s', id)
             return id
-        elif isfile(joinpath(self.srvdir, '.vagrant', 'machines',
-                             'default', self.provider, 'id')):
+        elif isfile(os.path.join(self.srvdir, '.vagrant', 'machines',
+                                 'default', self.provider, 'id')):
             # Vagrant 1.2 (and maybe 1.1?) it's a directory tree...
-            with open(joinpath(self.srvdir, '.vagrant', 'machines',
-                               'default', self.provider, 'id')) as f:
+            with open(os.path.join(self.srvdir, '.vagrant', 'machines',
+                                   'default', self.provider, 'id')) as f:
                 id = f.read()
                 logger.debug('vm uuid: %s', id)
             return id
@@ -230,8 +288,8 @@ class FDroidBuildVm():
             _check_call(['vagrant', 'box', 'remove', '--all', '--force', boxname])
         except subprocess.CalledProcessError as e:
             logger.debug('tried removing box %s, but is did not exist: %s', boxname, e)
-        boxpath = joinpath(expanduser('~'), '.vagrant',
-                           self._vagrant_file_name(boxname))
+        boxpath = os.path.join(expanduser('~'), '.vagrant',
+                               self._vagrant_file_name(boxname))
         if isdir(boxpath):
             logger.info("attempting to remove box '%s' by deleting: %s",
                         boxname, boxpath)
@@ -245,11 +303,13 @@ class FDroidBuildVm():
         """
         import paramiko
         try:
-            _check_call(['vagrant ssh-config > sshconfig'],
-                        cwd=self.srvdir, shell=True)
+            sshconfig_path = os.path.join(self.srvdir, 'sshconfig')
+            with open(sshconfig_path, 'wb') as fp:
+                fp.write(_check_output(['vagrant', 'ssh-config'],
+                                       cwd=self.srvdir))
             vagranthost = 'default'  # Host in ssh config file
             sshconfig = paramiko.SSHConfig()
-            with open(joinpath(self.srvdir, 'sshconfig'), 'r') as f:
+            with open(sshconfig_path, 'r') as f:
                 sshconfig.parse(f)
             sshconfig = sshconfig.lookup(vagranthost)
             idfile = sshconfig['identityfile']
@@ -297,16 +357,12 @@ class LibvirtBuildVm(FDroidBuildVm):
         # (eg. lookupByName only works on running VMs)
         try:
             _check_call(('virsh', '-c', 'qemu:///system', 'destroy', self.srvname))
-            logger.info("...waiting a sec...")
-            time.sleep(10)
         except subprocess.CalledProcessError as e:
             logger.info("could not force libvirt domain '%s' off: %s", self.srvname, e)
         try:
             # libvirt python bindings do not support all flags required
             # for undefining domains correctly.
             _check_call(('virsh', '-c', 'qemu:///system', 'undefine', self.srvname, '--nvram', '--managed-save', '--remove-all-storage', '--snapshots-metadata'))
-            logger.info("...waiting a sec...")
-            time.sleep(10)
         except subprocess.CalledProcessError as e:
             logger.info("could not undefine libvirt domain '%s': %s", self.srvname, e)
 
@@ -316,6 +372,7 @@ class LibvirtBuildVm(FDroidBuildVm):
             logger.debug('no output name set for packaging \'%s\',' +
                          'defaulting to %s', self.srvname, output)
         storagePool = self.conn.storagePoolLookupByName('default')
+        domainInfo = self.conn.lookupByName(self.srvname).info()
         if storagePool:
 
             if isfile('metadata.json'):
@@ -329,7 +386,9 @@ class LibvirtBuildVm(FDroidBuildVm):
             vol = storagePool.storageVolLookupByName(self.srvname + '.img')
             imagepath = vol.path()
             # TODO use a libvirt storage pool to ensure the img file is readable
-            _check_call(['sudo', '/bin/chmod', '-R', 'a+rX', '/var/lib/libvirt/images'])
+            if not os.access(imagepath, os.R_OK):
+                logger.warning(_('Cannot read "{path}"!').format(path=imagepath))
+                _check_call(['sudo', '/bin/chmod', '-R', 'a+rX', '/var/lib/libvirt/images'])
             shutil.copy2(imagepath, 'box.img')
             _check_call(['qemu-img', 'rebase', '-p', '-b', '', 'box.img'])
             img_info_raw = _check_output(['qemu-img', 'info', '--output=json', 'box.img'])
@@ -354,9 +413,11 @@ class LibvirtBuildVm(FDroidBuildVm):
                       libvirt.host = ""
                       libvirt.connect_via_ssh = false
                       libvirt.storage_pool_name = "default"
+                      libvirt.cpus = {cpus}
+                      libvirt.memory = {memory}
 
                     end
-                  end""")
+                  end""".format_map({'memory': str(int(domainInfo[1] / 1024)), 'cpus': str(domainInfo[3])}))
             with open('Vagrantfile', 'w') as fp:
                 fp.write(vagrantfile)
             with tarfile.open(output, 'w:gz') as tar:
@@ -398,8 +459,6 @@ class LibvirtBuildVm(FDroidBuildVm):
         logger.info("creating snapshot '%s' for vm '%s'", snapshot_name, self.srvname)
         try:
             _check_call(['virsh', '-c', 'qemu:///system', 'snapshot-create-as', self.srvname, snapshot_name])
-            logger.info('...waiting a sec...')
-            time.sleep(10)
         except subprocess.CalledProcessError as e:
             raise FDroidBuildVmException("could not cerate snapshot '%s' "
                                          "of libvirt vm '%s'"
@@ -428,8 +487,6 @@ class LibvirtBuildVm(FDroidBuildVm):
             dom = self.conn.lookupByName(self.srvname)
             snap = dom.snapshotLookupByName(snapshot_name)
             dom.revertToSnapshot(snap)
-            logger.info('...waiting a sec...')
-            time.sleep(10)
         except libvirt.libvirtError as e:
             raise FDroidBuildVmException('could not revert domain \'%s\' to snapshot \'%s\''
                                          % (self.srvname, snapshot_name)) from e
@@ -445,8 +502,6 @@ class VirtualboxBuildVm(FDroidBuildVm):
         logger.info("creating snapshot '%s' for vm '%s'", snapshot_name, self.srvname)
         try:
             _check_call(['VBoxManage', 'snapshot', self.srvuuid, 'take', 'fdroidclean'], cwd=self.srvdir)
-            logger.info('...waiting a sec...')
-            time.sleep(10)
         except subprocess.CalledProcessError as e:
             raise FDroidBuildVmException('could not cerate snapshot '
                                          'of virtualbox vm %s'