#!/usr/bin/python # Copyright (C) 2012-2015 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . '''A Morph deployment write extension for deploying to VirtualBox via ssh. VirtualBox is assumed to be running on a remote machine, which is accessed over ssh. The machine gets created, but not started. See file virtualbox-ssh.write.help for documentation ''' import cliapp import os import re import sys import time import tempfile import urlparse import morphlib.writeexts class VirtualBoxPlusSshWriteExtension(morphlib.writeexts.WriteExtension): def process_args(self, args): if len(args) != 2: raise cliapp.AppException('Wrong number of command line args') temp_root, location = args ssh_host, vm_name, vdi_path = self.parse_location(location) autostart = self.get_environment_boolean('AUTOSTART') vagrant = self.get_environment_boolean('VAGRANT') fd, raw_disk = tempfile.mkstemp() os.close(fd) self.create_local_system(temp_root, raw_disk) try: self.transfer_and_convert_to_vdi( raw_disk, ssh_host, vdi_path) self.create_virtualbox_guest(ssh_host, vm_name, vdi_path, autostart, vagrant) except BaseException: sys.stderr.write('Error deploying to VirtualBox') os.remove(raw_disk) cliapp.ssh_runcmd(ssh_host, ['rm', '-f', vdi_path]) raise else: os.remove(raw_disk) self.status( msg='Virtual machine %(vm_name)s has been created', vm_name=vm_name) def parse_location(self, location): '''Parse the location argument to get relevant data.''' x = urlparse.urlparse(location) if x.scheme != 'vbox+ssh': raise cliapp.AppException( 'URL schema must be vbox+ssh in %s' % location) m = re.match('^/(?P[^/]+)(?P/.+)$', x.path) if not m: raise cliapp.AppException('Cannot parse location %s' % location) return x.netloc, m.group('guest'), m.group('path') def transfer_and_convert_to_vdi(self, raw_disk, ssh_host, vdi_path): '''Transfer raw disk image to VirtualBox host, and convert to VDI.''' self.status(msg='Transfer disk and convert to VDI') st = os.lstat(raw_disk) xfer_hole_path = morphlib.util.get_data_path('xfer-hole') recv_hole = morphlib.util.get_data('recv-hole') ssh_remote_cmd = [ 'sh', '-c', recv_hole, 'dummy-argv0', 'vbox', vdi_path, str(st.st_size), ] cliapp.runcmd( ['python', xfer_hole_path, raw_disk], ['ssh', ssh_host] + map(cliapp.shell_quote, ssh_remote_cmd), stdout=None, stderr=None) def virtualbox_version(self, ssh_host): 'Get the version number of the VirtualBox running on the remote host.' # --version gives a build id, which looks something like # 1.2.3r456789, so we need to strip the suffix off and get a tuple # of the (major, minor, patch) version, since comparing with a # tuple is more reliable than a string and more convenient than # comparing against the major, minor and patch numbers directly self.status(msg='Checking version of remote VirtualBox') build_id = cliapp.ssh_runcmd(ssh_host, ['VBoxManage', '--version']) version_string = re.match(r"^([0-9\.]+).*$", build_id.strip()).group(1) return tuple(int(s or '0') for s in version_string.split('.')) def create_virtualbox_guest(self, ssh_host, vm_name, vdi_path, autostart, vagrant): '''Create the VirtualBox virtual machine.''' self.status(msg='Create VirtualBox virtual machine') ram_mebibytes = str(self.get_ram_size() / (1024**2)) vcpu_count = str(self.get_vcpu_count()) if not vagrant: hostonly_iface = self.get_host_interface(ssh_host) if self.virtualbox_version(ssh_host) < (4, 3, 0): sataportcount_option = '--sataportcount' else: sataportcount_option = '--portcount' commands = [ ['createvm', '--name', vm_name, '--ostype', 'Linux26_64', '--register'], ['modifyvm', vm_name, '--ioapic', 'on', '--memory', ram_mebibytes, '--cpus', vcpu_count], ['storagectl', vm_name, '--name', 'SATA Controller', '--add', 'sata', '--bootable', 'on', sataportcount_option, '2'], ['storageattach', vm_name, '--storagectl', 'SATA Controller', '--port', '0', '--device', '0', '--type', 'hdd', '--medium', vdi_path], ] if vagrant: commands[1].extend(['--nic1', 'nat', '--natnet1', 'default']) else: commands[1].extend(['--nic1', 'hostonly', '--hostonlyadapter1', hostonly_iface, '--nic2', 'nat', '--natnet2', 'default']) attach_disks = self.parse_attach_disks() for device_no, disk in enumerate(attach_disks, 1): cmd = ['storageattach', vm_name, '--storagectl', 'SATA Controller', '--port', str(device_no), '--device', '0', '--type', 'hdd', '--medium', disk] commands.append(cmd) if autostart: commands.append(['startvm', vm_name]) for command in commands: argv = ['VBoxManage'] + command cliapp.ssh_runcmd(ssh_host, argv) def get_host_interface(self, ssh_host): host_ipaddr = os.environ.get('HOST_IPADDR') netmask = os.environ.get('NETMASK') if host_ipaddr is None: raise cliapp.AppException('HOST_IPADDR was not given') if netmask is None: raise cliapp.AppException('NETMASK was not given') # 'VBoxManage list hostonlyifs' retrieves a list with the hostonly # interfaces on the host. For each interface, the following lines # are shown on top: # # Name: vboxnet0 # GUID: 786f6276-656e-4074-8000-0a0027000000 # Dhcp: Disabled # IPAddress: 192.168.100.1 # # The following command tries to retrieve the hostonly interface # name (e.g. vboxnet0) associated with the given ip address. iface = None lines = cliapp.ssh_runcmd(ssh_host, ['VBoxManage', 'list', 'hostonlyifs']).splitlines() for i, v in enumerate(lines): if host_ipaddr in v: iface = lines[i-3].split()[1] break if iface is None: iface = cliapp.ssh_runcmd(ssh_host, ['VBoxManage', 'hostonlyif', 'create']) # 'VBoxManage hostonlyif create' shows the name of the # created hostonly interface inside single quotes iface = iface[iface.find("'") + 1 : iface.rfind("'")] cliapp.ssh_runcmd(ssh_host, ['VBoxManage', 'hostonlyif', 'ipconfig', iface, '--ip', host_ipaddr, '--netmask', netmask]) return iface VirtualBoxPlusSshWriteExtension().run()