diff --git a/manifests/virtualbox-vagrant.manifest.json b/manifests/virtualbox-vagrant.manifest.json new file mode 100644 index 0000000..30609bd --- /dev/null +++ b/manifests/virtualbox-vagrant.manifest.json @@ -0,0 +1,43 @@ +{ + "provider": "virtualbox", + "bootstrapper": { + "workspace": "/target", + "guest_additions": "/root/images/VBoxGuestAdditions.iso" + }, + "image": { + "name": "debian-{system.release}-{system.architecture}-{%y}{%m}{%d}", + "description": "Debian {system.release} {system.architecture}" + }, + "system": { + "release": "wheezy", + "architecture": "amd64", + "timezone": "UTC", + "locale": "en_US", + "charmap": "UTF-8" + }, + "volume": { + "backing": "vmdk", + "partitions": { + "type": "mbr", + "boot": { + "size": 64, + "filesystem": "ext2" + }, + "root": { + "size": 1855, + "filesystem": "ext4" + }, + "swap": {"size": 128} + } + }, + "plugins": { + "admin_user": { + "username": "vagrant" + }, + "root_password": { + "password": "vagrant" + }, + "vagrant": { + } + } +} diff --git a/plugins/vagrant/__init__.py b/plugins/vagrant/__init__.py new file mode 100644 index 0000000..8ba5320 --- /dev/null +++ b/plugins/vagrant/__init__.py @@ -0,0 +1,33 @@ +import tasks + + +def validate_manifest(data, schema_validate): + from os import path + schema_path = path.normpath(path.join(path.dirname(__file__), 'manifest-schema.json')) + schema_validate(data, schema_path) + + +def resolve_tasks(tasklist, manifest): + from common.tasks import security + from common.tasks import loopback + tasklist.remove(security.DisableSSHPasswordAuthentication, + loopback.MoveImage, + ) + from common.tasks import volume + tasklist.add(tasks.CreateVagrantBoxDir, + tasks.AddPackages, + tasks.AddInsecurePublicKey, + tasks.PackageBox, + tasks.RemoveVagrantBoxDir, + volume.Delete, + ) + + +def resolve_rollback_tasks(tasklist, tasks_completed, manifest): + completed = [type(task) for task in tasks_completed] + + def counter_task(task, counter): + if task in completed and counter not in completed: + tasklist.add(counter) + + counter_task(tasks.CreateVagrantBoxDir, tasks.RemoveVagrantBoxDir) diff --git a/plugins/vagrant/assets/authorized_keys b/plugins/vagrant/assets/authorized_keys new file mode 100644 index 0000000..18a9c00 --- /dev/null +++ b/plugins/vagrant/assets/authorized_keys @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key diff --git a/plugins/vagrant/assets/box.ovf b/plugins/vagrant/assets/box.ovf new file mode 100644 index 0000000..b990dfd --- /dev/null +++ b/plugins/vagrant/assets/box.ovf @@ -0,0 +1,197 @@ + + + + + + + List of the virtual disks used in the package + + + + Logical networks used in the package + + Logical network used by this appliance. + + + + A virtual machine + + The kind of installed guest operating system + Debian_64 + Debian_64 + + + Virtual hardware requirements for a virtual machine + + Virtual Hardware Family + 0 + [BOXNAME] + virtualbox-2.2 + + + 1 virtual CPU + Number of virtual CPUs + 1 virtual CPU + 1 + 3 + 1 + + + MegaBytes + 256 MB of memory + Memory Size + 256 MB of memory + 2 + 4 + 256 + + + 0 + ideController0 + IDE Controller + ideController0 + 3 + PIIX4 + 5 + + + 1 + ideController1 + IDE Controller + ideController1 + 4 + PIIX4 + 5 + + + 0 + sataController0 + SATA Controller + sataController0 + 5 + AHCI + 20 + + + true + Ethernet adapter on 'NAT' + NAT + Ethernet adapter on 'NAT' + 6 + E1000 + 10 + + + 0 + disk1 + Disk Image + disk1 + /disk/vmdisk1 + 7 + 5 + 17 + + + 0 + true + cdrom1 + CD-ROM Drive + cdrom1 + 8 + 3 + 15 + + + 0 + true + cdrom2 + CD-ROM Drive + cdrom2 + 9 + 4 + 15 + + + + Complete VirtualBox machine configuration in VirtualBox format + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/vagrant/assets/metadata.json b/plugins/vagrant/assets/metadata.json new file mode 100644 index 0000000..4fc99bd --- /dev/null +++ b/plugins/vagrant/assets/metadata.json @@ -0,0 +1 @@ +{"provider": "virtualbox"} diff --git a/plugins/vagrant/manifest-schema.json b/plugins/vagrant/manifest-schema.json new file mode 100644 index 0000000..f432bd2 --- /dev/null +++ b/plugins/vagrant/manifest-schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Vagrant plugin manifest", + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["virtualbox"] + }, + "volume": { + "type": "object", + "properties": { + "backing": { + "type": "string", + "enum": ["vmdk"] + // VirtualBox only supports vmdk or raw when importing via OVF: + // https://www.virtualbox.org/browser/vbox/trunk/src/VBox/Main/src-server/ApplianceImplImport.cpp#L636 + } + }, + "required": ["backing"] + }, + "plugins": { + "type": "object", + "properties": { + "root_password": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "required": ["password"] + } + }, + "required": ["root_password"] + } + }, + "required": ["plugins"] +} diff --git a/plugins/vagrant/tasks.py b/plugins/vagrant/tasks.py new file mode 100644 index 0000000..0bf5fd8 --- /dev/null +++ b/plugins/vagrant/tasks.py @@ -0,0 +1,157 @@ +from base import Task +from common import phases +from common.tasks import workspace +from common.tasks import apt +from plugins.admin_user.tasks import CreateAdminUser +import os +import shutil +assets = os.path.normpath(os.path.join(os.path.dirname(__file__), 'assets')) + + +class CreateVagrantBoxDir(Task): + description = 'Creating directory for the vagrant box' + phase = phases.preparation + predecessors = [workspace.CreateWorkspace] + + def run(self, info): + info.vagrant_folder = os.path.join(info.workspace, 'vagrant') + os.mkdir(info.vagrant_folder) + + +class AddPackages(Task): + description = 'Add packages that vagrant depends on' + phase = phases.preparation + predecessors = [apt.AddDefaultSources] + + def run(self, info): + info.packages.add('openssh-server') + + +class AddInsecurePublicKey(Task): + description = 'Adding vagrant insecure public key' + phase = phases.system_modification + predecessors = [CreateAdminUser] + + def run(self, info): + ssh_dir = os.path.join(info.root, 'home/vagrant/.ssh') + os.mkdir(ssh_dir) + + authorized_keys_source_path = os.path.join(assets, 'authorized_keys') + with open(authorized_keys_source_path, 'r') as authorized_keys_source: + insecure_public_key = authorized_keys_source.read() + + authorized_keys_path = os.path.join(ssh_dir, 'authorized_keys') + with open(authorized_keys_path, 'a') as authorized_keys: + authorized_keys.write(insecure_public_key) + + +class PackageBox(Task): + description = 'Packaging the volume as a vagrant box' + phase = phases.image_registration + + def run(self, info): + box_basename = info.manifest.image['name'].format(**info.manifest_vars) + box_name = '{name}.box'.format(name=box_basename) + box_path = os.path.join(info.manifest.bootstrapper['workspace'], box_name) + + vagrantfile_source = os.path.join(assets, 'Vagrantfile') + vagrantfile = os.path.join(info.vagrant_folder, 'Vagrantfile') + shutil.copy(vagrantfile_source, vagrantfile) + + import random + mac_address = '080027{mac:06X}'.format(mac=random.randrange(16 ** 6)) + from common.tools import sed_i + sed_i(vagrantfile, '\\[MAC_ADDRESS\\]', mac_address) + + metadata_source = os.path.join(assets, 'metadata.json') + metadata = os.path.join(info.vagrant_folder, 'metadata.json') + shutil.copy(metadata_source, metadata) + + from common.tools import log_check_call + disk_name = 'box-disk1.{ext}'.format(ext=info.volume.extension) + disk_link = os.path.join(info.vagrant_folder, disk_name) + log_check_call(['ln', '-s', info.volume.image_path, disk_link]) + + ovf_path = os.path.join(info.vagrant_folder, 'box.ovf') + self.write_ovf(info, ovf_path, box_name, mac_address, disk_name) + + box_files = os.listdir(info.vagrant_folder) + log_check_call(['tar', '--create', '--gzip', '--dereference', + '--file', box_path, + '--directory', info.vagrant_folder] + + box_files + ) + import logging + logging.getLogger(__name__).info('The vagrant box has been placed at {box_path}' + .format(box_path=box_path)) + + def write_ovf(self, info, destination, box_name, mac_address, disk_name): + namespaces = {'ovf': 'http://schemas.dmtf.org/ovf/envelope/1', + 'rasd': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData', + 'vssd': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'vbox': 'http://www.virtualbox.org/ovf/machine', + } + + def attr(element, name, value=None): + for prefix, ns in namespaces.iteritems(): + name = name.replace(prefix + ':', '{' + ns + '}') + if value is None: + return element.attrib[name] + else: + element.attrib[name] = str(value) + + template_path = os.path.join(assets, 'box.ovf') + import xml.etree.ElementTree as ET + template = ET.parse(template_path) + root = template.getroot() + + [disk_ref] = root.findall('./ovf:References/ovf:File', namespaces) + attr(disk_ref, 'ovf:href', disk_name) + + # List of OVF disk format URIs + # Snatched from VBox source (src/VBox/Main/src-server/ApplianceImpl.cpp:47) + # ISOURI = "http://www.ecma-international.org/publications/standards/Ecma-119.htm" + # VMDKStreamURI = "http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" + # VMDKSparseURI = "http://www.vmware.com/specifications/vmdk.html#sparse" + # VMDKCompressedURI = "http://www.vmware.com/specifications/vmdk.html#compressed" + # VMDKCompressedURI2 = "http://www.vmware.com/interfaces/specifications/vmdk.html#compressed" + # VHDURI = "http://go.microsoft.com/fwlink/?LinkId=137171" + volume_uuid = info.volume.get_uuid() + [disk] = root.findall('./ovf:DiskSection/ovf:Disk', namespaces) + attr(disk, 'ovf:capacity', info.volume.partition_map.get_total_size() * 1024 * 1024) + attr(disk, 'ovf:format', info.volume.ovf_uri) + attr(disk, 'ovf:uuid', volume_uuid) + + [system] = root.findall('./ovf:VirtualSystem', namespaces) + attr(system, 'ovf:id', box_name) + + [sysid] = system.findall('./ovf:VirtualHardwareSection/ovf:System/' + 'vssd:VirtualSystemIdentifier', namespaces) + sysid.text = box_name + + [machine] = system.findall('./vbox:Machine', namespaces) + import uuid + attr(machine, 'ovf:uuid', uuid.uuid4()) + attr(machine, 'ovf:name', box_name) + from datetime import datetime + attr(machine, 'ovf:lastStateChange', datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')) + [nic] = machine.findall('./ovf:Hardware/ovf:Network/ovf:Adapter', namespaces) + attr(machine, 'ovf:MACAddress', mac_address) + + [device_img] = machine.findall('./ovf:StorageControllers' + '/ovf:StorageController[@name="SATA Controller"]' + '/ovf:AttachedDevice/ovf:Image', namespaces) + attr(device_img, 'ovf:uuid', '{' + str(volume_uuid) + '}') + + template.write(destination, xml_declaration=True) # , default_namespace=namespaces['ovf'] + + +class RemoveVagrantBoxDir(Task): + description = 'Removing the vagrant box directory' + phase = phases.cleaning + successors = [workspace.DeleteWorkspace] + + def run(self, info): + shutil.rmtree(info.vagrant_folder) + del info.vagrant_folder