blob: a76545838c919302688ddb9d3d6fbcb382f0d220 [file] [log] [blame]
Prashanth Balasubramanian3a9b9a12014-11-13 20:05:10 -08001#!/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
8The intent of this interface is to provde a layer of abstraction
9between the box providers and the creation of a lab cluster. To switch to a
10different 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
17Note that the second is optional because most providers honor
18`VAGRANT_DEFAULT_PROVIDER` directly in the template.
19"""
20
21
22import logging
23import subprocess
24import sys
25import os
26
27import common
28from autotest_lib.site_utils.lib import infra
29
30
31class 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.
40VAGRANT_DIR = os.path.abspath(os.path.join(__file__, os.pardir))
41
42class VagrantProvisioner(object):
43 """Provisiong vms with vagrant."""
44
45 # A path to a Vagrantfile template specific to the vm provider, specified
46 # in the child class.
47 _template = None
48 _box_name = 'base'
49
50
51 @classmethod
52 def vagrant_cmd(cls, cmd, stream_output=False):
53 """Execute a vagrant command in VAGRANT_DIR.
54
55 @param cmd: The command to execute.
56 @param stream_output: If True, stream the output of `cmd`.
57 Waits for `cmd` to finish and returns a string with the
58 output if false.
59 """
60 with infra.chdir(VAGRANT_DIR):
61 try:
62 return infra.execute_command(
63 'localhost',
64 'vagrant %s' % cmd, stream_output=stream_output)
65 except subprocess.CalledProcessError as e:
66 raise VagrantCmdError(
67 'Command "vagrant %s" failed with %s' % (cmd, e))
68
69
70 def _check_vagrant(self):
71 """Check Vagrant."""
72
73 # TODO: Automate the installation of vagrant.
74 try:
75 self.vagrant_cmd('--version')
76 except VagrantCmdError:
77 logging.error(
78 'Looks like you don\'t have vagrant. Please run: \n'
79 '`apt-get install virtualbox vagrant`. This assumes you '
80 'are on Trusty; There is a TODO to automate installation.')
81 sys.exit(1)
82
83
84 def __init__(self, puppet_path):
85 """Initialize a vagrant provisioner.
86
87 @param puppet_path: Since vagrant uses puppet to provision machines,
88 this is the location of puppet modules for various server roles.
89 """
90 self._check_vagrant()
91 self.puppet_path = puppet_path
92
93
94 def register_box(self, source, name=_box_name):
95 """Register a box with vagrant.
96
97 Eg: vagrant box add core_cluster chromeos_lab_core_cluster.box
98
99 @param source: A path to the box, typically a file path on localhost.
100 @param name: A name to register the box under.
101 """
102 if name in self.vagrant_cmd('box list'):
103 logging.warning("Name %s already in registry, will reuse.", name)
104 return
105 logging.info('Adding a new box from %s under name: %s', source, name)
106 self.vagrant_cmd('box add %s %s' % (name, source))
107
108
109 def unregister_box(self, name):
110 """Unregister a box.
111
112 Eg: vagrant box remove core_cluster.
113
114 @param name: The name of the box as it appears in `vagrant box list`
115 """
116 if name not in self.vagrant_cmd('box list'):
117 logging.warning("Name %s not in registry.", name)
118 return
119 logging.info('Removing box %s', name)
120 self.vagrant_cmd('box remove %s' % name)
121
122
123 def create_vagrant_file(self, **kwargs):
124 """Create a vagrant file.
125
126 Read the template, apply kwargs and the puppet_path so vagrant can find
127 server provisioning rules, and write it back out as the VagrantFile.
128
129 @param kwargs: Extra args needed to convert a template
130 to a real VagrantFile.
131 """
132 vagrant_file = os.path.join(VAGRANT_DIR, 'Vagrantfile')
133 kwargs.update({
134 'manifest_path': os.path.join(self.puppet_path, 'manifests'),
135 'module_path': os.path.join(self.puppet_path, 'modules'),
136 })
137 vagrant_template = ''
138 with open(self._template, 'r') as template:
139 vagrant_template = template.read()
140 with open(vagrant_file, 'w') as vagrantfile:
141 vagrantfile.write(vagrant_template % kwargs)
142
143
144 # TODO: This is a leaky abstraction, since it isn't really clear
145 # what the kwargs are. It's the best we can do, because the kwargs
146 # really need to match the VagrantFile. We leave parsing the VagrantFile
147 # for the right args upto the caller.
148 def initialize_vagrant(self, **kwargs):
149 """Initialize vagrant.
150
151 @param kwargs: The kwargs to pass to the VagrantFile.
152 Eg: {
153 'shard1': 'lumpyshard',
154 'shard1_port': 8002,
155 'shard1_shadow_config_hostname': 'localhost:8002',
156 }
157 @return: True if vagrant was initialized, False if the cwd already
158 contains a vagrant environment.
159 """
160 # TODO: Split this out. There are cases where we will need to
161 # reinitialize (by destroying all vms and recreating the VagrantFile)
162 # that we cannot do without manual intervention right now.
163 try:
164 self.vagrant_cmd('status')
165 logging.info('Vagrant already initialized in %s', VAGRANT_DIR)
166 return False
167 except VagrantCmdError:
168 logging.info('Initializing vagrant in %s', VAGRANT_DIR)
169 self.create_vagrant_file(**kwargs)
170 return True
171
172
173 def provision(self, force=False):
174 """Provision vms according to the vagrant file.
175
176 @param force: If True, vms in the VAGRANT_DIR will be destroyed and
177 reprovisioned.
178 """
179 if force:
180 logging.info('Destroying vagrant setup.')
181 try:
182 self.vagrant_cmd('destroy --force', stream_output=True)
183 except VagrantCmdError:
184 pass
185 logging.info('Starting vms. This should take no longer than 20 minutes '
186 'the first time, and no longer than 5 subsequently.')
187 self.vagrant_cmd('up', stream_output=True)
188
189
190class VirtualBox(VagrantProvisioner):
191 """A VirtualBoxProvisioner."""
192
193 _template = os.path.join(VAGRANT_DIR, 'ClusterTemplate')
194
195