#!/usr/bin/python # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Manage vms through vagrant. The intent of this interface is to provde a layer of abstraction between the box providers and the creation of a lab cluster. To switch to a different provider: * Create a VagrantFile template and specify _template in the subclass Eg: GCE VagrantFiles need a :google section * Override vagrant_cmd to massage parameters Eg: vagrant up => vagrant up --provider=google Note that the second is optional because most providers honor `VAGRANT_DEFAULT_PROVIDER` directly in the template. """ import logging import subprocess import sys import os import common from autotest_lib.site_utils.lib import infra class VagrantCmdError(Exception): """Raised when a vagrant command fails.""" # TODO: We don't really need to setup everythig in the same VAGRANT_DIR. # However managing vms becomes a headache once the VagrantFile and its # related dot files are removed, as one has to resort to directly # querying the box provider. Always running the cluster from the same # directory simplifies vm lifecycle management. VAGRANT_DIR = os.path.abspath(os.path.join(__file__, os.pardir)) VAGRANT_VERSION = '1.6.0' def format_msg(msg): """Format the give message. @param msg: A message to format out to stdout. """ print '\n{:^20s}%s'.format('') % msg class VagrantProvisioner(object): """Provisiong vms with vagrant.""" # A path to a Vagrantfile template specific to the vm provider, specified # in the child class. _template = None _box_name = 'base' @classmethod def vagrant_cmd(cls, cmd, stream_output=False): """Execute a vagrant command in VAGRANT_DIR. @param cmd: The command to execute. @param stream_output: If True, stream the output of `cmd`. Waits for `cmd` to finish and returns a string with the output if false. """ with infra.chdir(VAGRANT_DIR): try: return infra.execute_command( 'localhost', 'vagrant %s' % cmd, stream_output=stream_output) except subprocess.CalledProcessError as e: raise VagrantCmdError( 'Command "vagrant %s" failed with %s' % (cmd, e)) def _check_vagrant(self): """Check Vagrant.""" # TODO: Automate the installation of vagrant. try: version = int(self.vagrant_cmd('--version').rstrip('\n').rsplit( ' ')[-1].replace('.', '')) except VagrantCmdError: logging.error( 'Looks like you don\'t have vagrant. Please run: \n' '`apt-get install virtualbox vagrant`. This assumes you ' 'are on Trusty; There is a TODO to automate installation.') sys.exit(1) except TypeError as e: logging.warning('The format of the vagrant version string seems to ' 'have changed, assuming you have a version > %s.', VAGRANT_VERSION) return if version < int(VAGRANT_VERSION.replace('.', '')): logging.error('Please upgrade vagrant to a version > %s by ' 'downloading a deb file from ' 'https://www.vagrantup.com/downloads and installing ' 'it with dpkg -i file.deb', VAGRANT_VERSION) sys.exit(1) def __init__(self, puppet_path): """Initialize a vagrant provisioner. @param puppet_path: Since vagrant uses puppet to provision machines, this is the location of puppet modules for various server roles. """ self._check_vagrant() self.puppet_path = puppet_path def register_box(self, source, name=_box_name): """Register a box with vagrant. Eg: vagrant box add core_cluster chromeos_lab_core_cluster.box @param source: A path to the box, typically a file path on localhost. @param name: A name to register the box under. """ if name in self.vagrant_cmd('box list'): logging.warning("Name %s already in registry, will reuse.", name) return logging.info('Adding a new box from %s under name: %s', source, name) self.vagrant_cmd('box add %s %s' % (name, source)) def unregister_box(self, name): """Unregister a box. Eg: vagrant box remove core_cluster. @param name: The name of the box as it appears in `vagrant box list` """ if name not in self.vagrant_cmd('box list'): logging.warning("Name %s not in registry.", name) return logging.info('Removing box %s', name) self.vagrant_cmd('box remove %s' % name) def create_vagrant_file(self, **kwargs): """Create a vagrant file. Read the template, apply kwargs and the puppet_path so vagrant can find server provisioning rules, and write it back out as the VagrantFile. @param kwargs: Extra args needed to convert a template to a real VagrantFile. """ vagrant_file = os.path.join(VAGRANT_DIR, 'Vagrantfile') kwargs.update({ 'manifest_path': os.path.join(self.puppet_path, 'manifests'), 'module_path': os.path.join(self.puppet_path, 'modules'), }) vagrant_template = '' with open(self._template, 'r') as template: vagrant_template = template.read() with open(vagrant_file, 'w') as vagrantfile: vagrantfile.write(vagrant_template % kwargs) # TODO: This is a leaky abstraction, since it isn't really clear # what the kwargs are. It's the best we can do, because the kwargs # really need to match the VagrantFile. We leave parsing the VagrantFile # for the right args upto the caller. def initialize_vagrant(self, **kwargs): """Initialize vagrant. @param kwargs: The kwargs to pass to the VagrantFile. Eg: { 'shard1': 'stumpyshard', 'shard1_port': 8002, 'shard1_shadow_config_hostname': 'localhost:8002', } @return: True if vagrant was initialized, False if the cwd already contains a vagrant environment. """ # TODO: Split this out. There are cases where we will need to # reinitialize (by destroying all vms and recreating the VagrantFile) # that we cannot do without manual intervention right now. try: self.vagrant_cmd('status') logging.info('Vagrant already initialized in %s', VAGRANT_DIR) return False except VagrantCmdError: logging.info('Initializing vagrant in %s', VAGRANT_DIR) self.create_vagrant_file(**kwargs) return True def provision(self, force=False): """Provision vms according to the vagrant file. @param force: If True, vms in the VAGRANT_DIR will be destroyed and reprovisioned. """ if force: logging.info('Destroying vagrant setup.') try: self.vagrant_cmd('destroy --force', stream_output=True) except VagrantCmdError: pass format_msg('Starting vms. This should take no longer than 5 minutes') self.vagrant_cmd('up', stream_output=True) class VirtualBox(VagrantProvisioner): """A VirtualBoxProvisioner.""" _template = os.path.join(VAGRANT_DIR, 'ClusterTemplate')