# 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):
+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)
+ return subprocess.check_call(cmd, shell=False, cwd=cwd)
-def _check_output(cmd, shell=False):
+def _check_output(cmd, cwd=None):
logger.debug(' '.join(cmd))
- return subprocess.check_output(cmd, shell=shell)
+ return subprocess.check_output(cmd, shell=False, cwd=cwd)
def get_build_vm(srvdir, provider=None):
except subprocess.CalledProcessError:
pass
try:
- vbox_installed = 0 == _check_call(['which', 'VBoxHeadless'], shell=True)
+ vbox_installed = 0 == _check_call(['which', 'VBoxHeadless'])
except subprocess.CalledProcessError:
vbox_installed = False
if kvm_installed and vbox_installed:
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)
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))
if not isfile(self.vgrntfile):
import vagrant
self.vgrnt = vagrant.Vagrant(root=srvdir, out_cm=vagrant.stdout_cm, err_cm=vagrant.stdout_cm)
- def check_okay(self):
- return True
-
def up(self, provision=True):
- try:
- self.vgrnt.up(provision=provision)
- logger.info('...waiting a sec...')
- time.sleep(10)
- except subprocess.CalledProcessError as e:
- raise FDroidBuildVmException("could not bring up vm '%s'" % self.srvname) from e
-
- def snapshot_create(self, name):
- raise NotImplementedError('not implemented, please use a sub-type instance')
+ 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.
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)
except subprocess.CalledProcessError as e:
logger.debug('pruning global vagrant status failed: %s', e)
- def package(self, output=None, vagrantfile=None, keep_box_file=None):
- previous_tmp_dir = joinpath(self.srvdir, '_tmp_package')
- if isdir(previous_tmp_dir):
- logger.info('found previous vagrant package temp dir \'%s\', deleting it', previous_tmp_dir)
- shutil.rmtree(previous_tmp_dir)
- self.vgrnt.package(output=output, vagrantfile=vagrantfile)
+ def package(self, output=None):
+ self.vgrnt.package(output=output)
+
+ def vagrant_uuid_okay(self):
+ '''Having an uuid means that vagrant up has run successfully.'''
+ if self.srvuuid is None:
+ return False
+ return True
def _vagrant_file_name(self, name):
return name.replace('/', '-VAGRANTSLASH-')
+ def _vagrant_fetch_uuid(self):
+ if isfile(os.path.join(self.srvdir, '.vagrant')):
+ # Vagrant 1.0 - it's a json file...
+ 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(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(os.path.join(self.srvdir, '.vagrant', 'machines',
+ 'default', self.provider, 'id')) as f:
+ id = f.read()
+ logger.debug('vm uuid: %s', id)
+ return id
+ else:
+ logger.debug('vm uuid is None')
+ return None
+
def box_add(self, boxname, boxfile, force=True):
"""Add vagrant box to vagrant.
_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)
shutil.rmtree(boxpath)
+ def sshinfo(self):
+ """Get ssh connection info for a vagrant VM
+
+ :returns: A dictionary containing 'hostname', 'port', 'user'
+ and 'idfile'
+ """
+ import paramiko
+ try:
+ 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(sshconfig_path, 'r') as f:
+ sshconfig.parse(f)
+ sshconfig = sshconfig.lookup(vagranthost)
+ idfile = sshconfig['identityfile']
+ if isinstance(idfile, list):
+ idfile = idfile[0]
+ elif idfile.startswith('"') and idfile.endswith('"'):
+ idfile = idfile[1:-1]
+ return {'hostname': sshconfig['hostname'],
+ 'port': int(sshconfig['port']),
+ 'user': sshconfig['user'],
+ 'idfile': idfile}
+ except subprocess.CalledProcessError as e:
+ raise FDroidBuildVmException("Error getting ssh config") from e
+
+ def snapshot_create(self, snapshot_name):
+ raise NotImplementedError('not implemented, please use a sub-type instance')
+
+ def snapshot_list(self):
+ raise NotImplementedError('not implemented, please use a sub-type instance')
+
+ def snapshot_exists(self, snapshot_name):
+ raise NotImplementedError('not implemented, please use a sub-type instance')
+
+ def snapshot_revert(self, snapshot_name):
+ raise NotImplementedError('not implemented, please use a sub-type instance')
+
class LibvirtBuildVm(FDroidBuildVm):
def __init__(self, srvdir):
+ self.provider = 'libvirt'
super().__init__(srvdir)
import libvirt
except libvirt.libvirtError as e:
raise FDroidBuildVmException('could not connect to libvirtd: %s' % (e))
- def check_okay(self):
- import libvirt
- imagepath = joinpath('var', 'lib', 'libvirt', 'images',
- '%s.img' % self._vagrant_file_name(self.srvname))
- image_present = False
- if isfile(imagepath):
- image_present = True
- try:
- self.conn.lookupByName(self.srvname)
- domain_defined = True
- except libvirt.libvirtError:
- pass
- if image_present and domain_defined:
- return True
- return False
-
def destroy(self):
super().destroy()
# (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)
- def package(self, output=None, vagrantfile=None, keep_box_file=False):
+ def package(self, output=None, keep_box_file=False):
if not output:
output = "buildserver.box"
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'):
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'])
"virtual_size": math.ceil(img_info['virtual-size'] / (1024. ** 3)),
}
- if not vagrantfile:
- logger.debug('no Vagrantfile supplied for box, generating a minimal one...')
- vagrantfile = 'Vagrant.configure("2") do |config|\nend'
-
logger.debug('preparing metadata.json for box %s', output)
with open('metadata.json', 'w') as fp:
fp.write(json.dumps(metadata))
logger.debug('preparing Vagrantfile for box %s', output)
+ vagrantfile = textwrap.dedent("""\
+ Vagrant.configure("2") do |config|
+ config.ssh.username = "vagrant"
+ config.ssh.password = "vagrant"
+
+ config.vm.provider :libvirt do |libvirt|
+
+ libvirt.driver = "kvm"
+ libvirt.host = ""
+ libvirt.connect_via_ssh = false
+ libvirt.storage_pool_name = "default"
+ libvirt.cpus = {cpus}
+ libvirt.memory = {memory}
+
+ 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:
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'"
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
class VirtualboxBuildVm(FDroidBuildVm):
+
+ def __init__(self, srvdir):
+ self.provider = 'virtualbox'
+ super().__init__(srvdir)
+
def snapshot_create(self, snapshot_name):
- raise NotImplemented('TODO')
+ logger.info("creating snapshot '%s' for vm '%s'", snapshot_name, self.srvname)
try:
- _check_call(['VBoxManage', 'snapshot', self.srvname, 'take', 'fdroidclean'], cwd=self.srvdir)
- logger.info('...waiting a sec...')
- time.sleep(10)
+ _check_call(['VBoxManage', 'snapshot', self.srvuuid, 'take', 'fdroidclean'], cwd=self.srvdir)
except subprocess.CalledProcessError as e:
raise FDroidBuildVmException('could not cerate snapshot '
'of virtualbox vm %s'
% self.srvname) from e
- def snapshot_available(self, snapshot_name):
- raise NotImplemented('TODO')
+ def snapshot_list(self):
+ try:
+ o = _check_output(['VBoxManage', 'snapshot',
+ self.srvuuid, 'list',
+ '--details'], cwd=self.srvdir)
+ return o
+ except subprocess.CalledProcessError as e:
+ raise FDroidBuildVmException("could not list snapshots "
+ "of virtualbox vm '%s'"
+ % (self.srvname)) from e
+
+ def snapshot_exists(self, snapshot_name):
+ try:
+ return str(snapshot_name) in str(self.snapshot_list())
+ except FDroidBuildVmException:
+ return False
+
+ def snapshot_revert(self, snapshot_name):
+ logger.info("reverting vm '%s' to snapshot '%s'",
+ self.srvname, snapshot_name)
+ try:
+ _check_call(['VBoxManage', 'snapshot', self.srvuuid,
+ 'restore', 'fdroidclean'], cwd=self.srvdir)
+ except subprocess.CalledProcessError as e:
+ raise FDroidBuildVmException("could not load snapshot "
+ "'fdroidclean' for vm '%s'"
+ % (self.srvname)) from e