Home | History | Annotate | Download | only in puppylab
      1 #!/usr/bin/python
      2 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Manage vms through vagrant.
      7 
      8 The intent of this interface is to provde a layer of abstraction
      9 between the box providers and the creation of a lab cluster. To switch to a
     10 different provider:
     11 
     12 * Create a VagrantFile template and specify _template in the subclass
     13   Eg: GCE VagrantFiles need a :google section
     14 * Override vagrant_cmd to massage parameters
     15   Eg: vagrant up => vagrant up --provider=google
     16 
     17 Note that the second is optional because most providers honor
     18 `VAGRANT_DEFAULT_PROVIDER` directly in the template.
     19 """
     20 
     21 
     22 import logging
     23 import subprocess
     24 import sys
     25 import os
     26 
     27 import common
     28 from autotest_lib.site_utils.lib import infra
     29 
     30 
     31 class VagrantCmdError(Exception):
     32     """Raised when a vagrant command fails."""
     33 
     34 
     35 # TODO: We don't really need to setup everythig in the same VAGRANT_DIR.
     36 # However managing vms becomes a headache once the VagrantFile and its
     37 # related dot files are removed, as one has to resort to directly
     38 # querying the box provider. Always running the cluster from the same
     39 # directory simplifies vm lifecycle management.
     40 VAGRANT_DIR = os.path.abspath(os.path.join(__file__, os.pardir))
     41 VAGRANT_VERSION = '1.6.0'
     42 
     43 
     44 def format_msg(msg):
     45     """Format the give message.
     46 
     47     @param msg: A message to format out to stdout.
     48     """
     49     print '\n{:^20s}%s'.format('') % msg
     50 
     51 
     52 class VagrantProvisioner(object):
     53     """Provisiong vms with vagrant."""
     54 
     55     # A path to a Vagrantfile template specific to the vm provider, specified
     56     # in the child class.
     57     _template = None
     58     _box_name = 'base'
     59 
     60 
     61     @classmethod
     62     def vagrant_cmd(cls, cmd, stream_output=False):
     63         """Execute a vagrant command in VAGRANT_DIR.
     64 
     65         @param cmd: The command to execute.
     66         @param stream_output: If True, stream the output of `cmd`.
     67                 Waits for `cmd` to finish and returns a string with the
     68                 output if false.
     69         """
     70         with infra.chdir(VAGRANT_DIR):
     71             try:
     72                 return infra.execute_command(
     73                         'localhost',
     74                         'vagrant %s' % cmd, stream_output=stream_output)
     75             except subprocess.CalledProcessError as e:
     76                 raise VagrantCmdError(
     77                         'Command "vagrant %s" failed with %s' % (cmd, e))
     78 
     79 
     80     def _check_vagrant(self):
     81         """Check Vagrant."""
     82 
     83         # TODO: Automate the installation of vagrant.
     84         try:
     85             version = int(self.vagrant_cmd('--version').rstrip('\n').rsplit(
     86                     ' ')[-1].replace('.', ''))
     87         except VagrantCmdError:
     88             logging.error(
     89                     'Looks like you don\'t have vagrant. Please run: \n'
     90                     '`apt-get install virtualbox vagrant`. This assumes you '
     91                     'are on Trusty; There is a TODO to automate installation.')
     92             sys.exit(1)
     93         except TypeError as e:
     94             logging.warning('The format of the vagrant version string seems to '
     95                             'have changed, assuming you have a version > %s.',
     96                             VAGRANT_VERSION)
     97             return
     98         if version < int(VAGRANT_VERSION.replace('.', '')):
     99             logging.error('Please upgrade vagrant to a version > %s by '
    100                           'downloading a deb file from '
    101                           'https://www.vagrantup.com/downloads and installing '
    102                           'it with dpkg -i file.deb', VAGRANT_VERSION)
    103             sys.exit(1)
    104 
    105 
    106     def __init__(self, puppet_path):
    107         """Initialize a vagrant provisioner.
    108 
    109         @param puppet_path: Since vagrant uses puppet to provision machines,
    110                 this is the location of puppet modules for various server roles.
    111         """
    112         self._check_vagrant()
    113         self.puppet_path = puppet_path
    114 
    115 
    116     def register_box(self, source, name=_box_name):
    117         """Register a box with vagrant.
    118 
    119         Eg: vagrant box add core_cluster chromeos_lab_core_cluster.box
    120 
    121         @param source: A path to the box, typically a file path on localhost.
    122         @param name: A name to register the box under.
    123         """
    124         if name in self.vagrant_cmd('box list'):
    125             logging.warning("Name %s already in registry, will reuse.", name)
    126             return
    127         logging.info('Adding a new box from %s under name: %s', source, name)
    128         self.vagrant_cmd('box add %s %s' % (name, source))
    129 
    130 
    131     def unregister_box(self, name):
    132         """Unregister a box.
    133 
    134         Eg: vagrant box remove core_cluster.
    135 
    136         @param name: The name of the box as it appears in `vagrant box list`
    137         """
    138         if name not in self.vagrant_cmd('box list'):
    139             logging.warning("Name %s not in registry.", name)
    140             return
    141         logging.info('Removing box %s', name)
    142         self.vagrant_cmd('box remove %s' % name)
    143 
    144 
    145     def create_vagrant_file(self, **kwargs):
    146         """Create a vagrant file.
    147 
    148         Read the template, apply kwargs and the puppet_path so vagrant can find
    149         server provisioning rules, and write it back out as the VagrantFile.
    150 
    151         @param kwargs: Extra args needed to convert a template
    152                 to a real VagrantFile.
    153         """
    154         vagrant_file = os.path.join(VAGRANT_DIR, 'Vagrantfile')
    155         kwargs.update({
    156             'manifest_path': os.path.join(self.puppet_path, 'manifests'),
    157             'module_path': os.path.join(self.puppet_path, 'modules'),
    158         })
    159         vagrant_template = ''
    160         with open(self._template, 'r') as template:
    161             vagrant_template = template.read()
    162         with open(vagrant_file, 'w') as vagrantfile:
    163             vagrantfile.write(vagrant_template % kwargs)
    164 
    165 
    166     # TODO: This is a leaky abstraction, since it isn't really clear
    167     # what the kwargs are. It's the best we can do, because the kwargs
    168     # really need to match the VagrantFile. We leave parsing the VagrantFile
    169     # for the right args upto the caller.
    170     def initialize_vagrant(self, **kwargs):
    171         """Initialize vagrant.
    172 
    173         @param kwargs: The kwargs to pass to the VagrantFile.
    174             Eg: {
    175                 'shard1': 'stumpyshard',
    176                 'shard1_port': 8002,
    177                 'shard1_shadow_config_hostname': 'localhost:8002',
    178             }
    179         @return: True if vagrant was initialized, False if the cwd already
    180                  contains a vagrant environment.
    181         """
    182         # TODO: Split this out. There are cases where we will need to
    183         # reinitialize (by destroying all vms and recreating the VagrantFile)
    184         # that we cannot do without manual intervention right now.
    185         try:
    186             self.vagrant_cmd('status')
    187             logging.info('Vagrant already initialized in %s', VAGRANT_DIR)
    188             return False
    189         except VagrantCmdError:
    190             logging.info('Initializing vagrant in %s', VAGRANT_DIR)
    191             self.create_vagrant_file(**kwargs)
    192             return True
    193 
    194 
    195     def provision(self, force=False):
    196         """Provision vms according to the vagrant file.
    197 
    198         @param force: If True, vms in the VAGRANT_DIR will be destroyed and
    199                 reprovisioned.
    200         """
    201         if force:
    202             logging.info('Destroying vagrant setup.')
    203             try:
    204                 self.vagrant_cmd('destroy --force', stream_output=True)
    205             except VagrantCmdError:
    206                 pass
    207         format_msg('Starting vms. This should take no longer than 5 minutes')
    208         self.vagrant_cmd('up', stream_output=True)
    209 
    210 
    211 class VirtualBox(VagrantProvisioner):
    212     """A VirtualBoxProvisioner."""
    213 
    214     _template = os.path.join(VAGRANT_DIR, 'ClusterTemplate')
    215