├── Makefile ├── com.github.vserv.monitor.plist ├── README.md └── vserv.py /Makefile: -------------------------------------------------------------------------------- 1 | USE_PKGBUILD=1 2 | include /usr/local/share/luggage/luggage.make 3 | PACKAGE_VERSION=1.0.0 4 | TITLE=vserv 5 | REVERSE_DOMAIN=com.github.vserv 6 | PAYLOAD= \ 7 | pack-Library-LaunchAgents-com.github.vserv.monitor.plist \ 8 | pack-usr-local-bin-vserv.py -------------------------------------------------------------------------------- /com.github.vserv.monitor.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | KeepAlive 6 | 7 | Label 8 | com.github.vserv.monitor 9 | ProgramArguments 10 | 11 | /usr/local/bin/vserv.py 12 | --monitor 13 | 14 | RunAtLoad 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vserv 2 | ----- 3 | 4 | ###I've incorporated this into the main [vfuse repo](https://github.com/chilcote/vfuse) and won't be updated any further in this repo. 5 | 6 | Service to monitor one or more vmx path[s] and restart the vmx[s] if necessary. 7 | 8 | The idea here is to automate the spinning up or tearing down of VMware Fusion VMs. Support has been added to the [vfuse](https://github.com/chilcote/vfuse) script, allowing one to automate tests or perhaps, with some ingenuity, deploy VMs as a service. 9 | 10 | This script was inspired by conversations wtih [Gilbert Wilson](https://twitter.com/boyonwheels) at our nascent Seattle [MacHackNight meetup](https://groups.google.com/forum/#!searchin/macenterprise/ctdawe$20red$20door/macenterprise/T2L2j8SnNWc/eUcghNez5sMJ), and is built upon the superior ideas illustrated [here](https://github.com/boyonwheels/vmrun.wrapper). 11 | 12 | Requirements 13 | ------------ 14 | 15 | + VMware Fusion 7.x Professional 16 | + OS X 10.10.2+ 17 | + python 2.7.x 18 | 19 | Usage 20 | ----- 21 | 22 | usage: vserv.py [-h] [--list] [--monitor] [--add ADD] [--remove REMOVE] 23 | [--reset RESET] [--get-ip GET_IP] 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | --list List monitored and running VMs 28 | --monitor Service to monitor VMs 29 | --add ADD Add monitoring for /path/to/vmx 30 | --remove REMOVE Remove monitoring for /path/to/vmx 31 | --reset RESET Reset /path/to/vmx 32 | --get-ip GET_IP Get IP for /path/to/vmx 33 | 34 | Monitoring VMs 35 | -------------- 36 | 37 | To manually run the service, use the `--monitor` argument. 38 | 39 | ./vserv.py --monitor 40 | 41 | Alternatively, you can copy the script to /usr/local/bin/ and the launchagent to /Library/LaunchAgents/ and initiate the service with: 42 | 43 | launchctl load /Library/LaunchAgents/com.github.vserv.monitor.plist 44 | 45 | Logging uses syslog, so you can watch progress with: 46 | 47 | tail -f /var/log/syslog |grep vserv 48 | 49 | 50 | Adding/Removing VMs 51 | ------------------- 52 | 53 | To monitor a vmx file, use the `--add` argument. 54 | 55 | ./vserv.py --add /path/to/vmx 56 | 57 | To stop monitoring a vmx file, use the `--remove` argument. 58 | 59 | ./vserv.py --remove /path/to/vmx 60 | 61 | To reset a vm, use the `--reset` argument. 62 | 63 | ./vserv.py --reset /path/to/vmx 64 | 65 | To list all monitored and running VMs, use the `--list` argument. 66 | 67 | ./vserv.py --list 68 | 69 | 70 | License 71 | ------- 72 | 73 | Copyright 2015 Joseph Chilcote 74 | 75 | Licensed under the Apache License, Version 2.0 (the "License"); 76 | you may not use this file except in compliance with the License. 77 | You may obtain a copy of the License at 78 | 79 | http://www.apache.org/licenses/LICENSE-2.0 80 | 81 | Unless required by applicable law or agreed to in writing, software 82 | distributed under the License is distributed on an "AS IS" BASIS, 83 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 84 | See the License for the specific language governing permissions and 85 | limitations under the License. 86 | -------------------------------------------------------------------------------- /vserv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Service to monitor one or more vmx path[s] and restart the vmx[s] if necessary 4 | 5 | Requirements: 6 | VMware Fusion 7.x Professional 7 | OS X 10.9.5+ (compatible with 10.10) 8 | 9 | usage: vserv.py [-h] [--list] [--monitor] [--add ADD] [--remove REMOVE] 10 | [--reset RESET] [--get-ip GET_IP] 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | --list List monitored and running VMs 15 | --monitor Service to monitor VMs 16 | --add ADD Add monitoring for /path/to/vmx 17 | --remove REMOVE Remove monitoring for /path/to/vmx 18 | --reset RESET Reset /path/to/vmx 19 | --get-ip GET_IP Get IP for /path/to/vmx 20 | ''' 21 | ############################################################################## 22 | # Copyright 2015 Joseph Chilcote 23 | # 24 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 25 | # use this file except in compliance with the License. You may obtain a copy 26 | # of the License at 27 | # 28 | # http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | # Unless required by applicable law or agreed to in writing, software 31 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 32 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 33 | # License for the specific language governing permissions and limitations 34 | # under the License. 35 | ############################################################################## 36 | 37 | __author__ = 'Joseph Chilcote (chilcote@gmail.com)' 38 | __version__ = '0.1.0' 39 | 40 | import os, sys, subprocess 41 | import plistlib 42 | import argparse 43 | import time 44 | import shutil 45 | import syslog 46 | from random import randint 47 | 48 | class VMXMonitor(object): 49 | '''Monitoring a vmx file''' 50 | 51 | def __init__(self, 52 | plist = os.path.expanduser('~/Library/Preferences/com.github.vserv.plist'), 53 | vmrun = '/Applications/VMware Fusion.app/Contents/Library/vmrun'): 54 | self.plist = plist 55 | if not os.path.exists(self.plist): 56 | plistlib.writePlist({}, plist) 57 | self.d = plistlib.readPlist(plist) 58 | self.vmrun = vmrun 59 | 60 | def set_start(self, vmx, job_id=None): 61 | if not self.d or not vmx in self.d.values()[0]: 62 | if not job_id: 63 | job_id = randint(1000, 9999) 64 | self.d[str(job_id)] = [str(vmx), 'start', '', ''] 65 | plistlib.writePlist(self.d, self.plist) 66 | 67 | def set_stop(self, vmx, job_id=None): 68 | for k, v in self.d.items(): 69 | if v[0] == vmx: 70 | self.d[k][1] = 'stop' 71 | plistlib.writePlist(self.d, self.plist) 72 | 73 | def set_reset(self, vmx, job_id=None): 74 | for k, v in self.d.items(): 75 | if v[0] == vmx: 76 | self.d[k][1] = 'reset' 77 | plistlib.writePlist(self.d, self.plist) 78 | 79 | def remove(self, vmx): 80 | for k, v in self.d.items(): 81 | if v[0] == vmx: 82 | del self.d[k] 83 | plistlib.writePlist(self.d, self.plist) 84 | 85 | def set_running(self, vmx): 86 | for k, v in self.d.items(): 87 | if v[0] == vmx: 88 | self.d[k][1] = 'running' 89 | self.d[k][2] = self.get_ip(vmx) 90 | self.d[k][3] = str(self.get_pid(vmx)) 91 | plistlib.writePlist(self.d, self.plist) 92 | 93 | def get_pid(self, vmx): 94 | for root, dirs, files in os.walk('/var/run/vmware'): 95 | for i in files: 96 | full_path = root + '/' + i 97 | if os.path.realpath(full_path) in vmx: 98 | return os.path.dirname(full_path).split('_')[-1] 99 | 100 | def get_monitored(self): 101 | l = [] 102 | for i in self.d.values(): 103 | l.append(i[0]) 104 | return l 105 | 106 | def is_running(self, vmx): 107 | for root, dirs, files in os.walk('/var/run/vmware'): 108 | for i in files: 109 | full_path = root + '/' + i 110 | if os.path.realpath(full_path) in vmx: 111 | return True 112 | 113 | def is_scheduled(self, vmx): 114 | for k, v in self.d.items(): 115 | if v[0] == vmx: 116 | if v[1] == 'start' or v[1] == 'running': 117 | return True 118 | 119 | def to_be_removed(self, vmx): 120 | for k, v in self.d.items(): 121 | if v[0] == vmx: 122 | if v[1] == 'stop': 123 | return True 124 | 125 | def to_be_reset(self, vmx): 126 | for k, v in self.d.items(): 127 | if v[0] == vmx: 128 | if v[1] == 'reset': 129 | return True 130 | 131 | def get_scheduled(self): 132 | d = {} 133 | for k, v in self.d.items(): 134 | if v[1] == 'start' or v[1] == 'running': 135 | d[k] = v 136 | return d 137 | 138 | def get_running(self): 139 | d = {} 140 | cmd = [self.vmrun, 'list'] 141 | output = subprocess.check_output(cmd) 142 | for line in output.splitlines(): 143 | if 'Total running VMs' in line: 144 | if int(line.split(': ')[1]) == 0: 145 | return d 146 | else: 147 | d[line.strip()] = self.get_pid(line.strip()) 148 | return d 149 | 150 | def start_vmx(self, vmx): 151 | cmd = [self.vmrun, 'start', vmx] 152 | task = subprocess.Popen(cmd, stdout=subprocess.PIPE, 153 | stderr=subprocess.PIPE) 154 | out, err = task.communicate() 155 | if err: 156 | syslog.syslog(syslog.LOG_ALERT, 'Could not process:\t%s' % vmx) 157 | 158 | def stop_vmx(self, vmx): 159 | cmd = [self.vmrun, 'stop', vmx] 160 | task = subprocess.Popen(cmd, stdout=subprocess.PIPE, 161 | stderr=subprocess.PIPE) 162 | out, err = task.communicate() 163 | if err: 164 | syslog.syslog(syslog.LOG_ALERT, 'Could not process:\t%s' % vmx) 165 | 166 | def reset_vmx(self, vmx): 167 | cmd = [self.vmrun, 'reset', vmx] 168 | task = subprocess.Popen(cmd, stdout=subprocess.PIPE, 169 | stderr=subprocess.PIPE) 170 | out, err = task.communicate() 171 | if err: 172 | syslog.syslog(syslog.LOG_ALERT, 'Could not process:\t%s' % vmx) 173 | 174 | def delete_vm(self, vmx): 175 | if os.path.exists(os.path.dirname(vmx)): 176 | shutil.rmtree(os.path.dirname(vmx)) 177 | 178 | def job_to_vmx(self, job_id): 179 | for k, v in self.d.items(): 180 | if job_id == k: 181 | return v[0] 182 | 183 | def get_ip(self, vmx): 184 | count = 1 185 | while count <= 36: 186 | cmd = [self.vmrun, 'getGuestIPAddress', vmx] 187 | task = subprocess.Popen(cmd, stdout=subprocess.PIPE, 188 | stderr=subprocess.PIPE) 189 | out, err = task.communicate() 190 | if not task.returncode == 255: 191 | break 192 | count += 1 193 | time.sleep(5) 194 | if 'Error' in out: 195 | return '' 196 | return out.strip() 197 | 198 | def vmx_is_valid(self, vmx): 199 | if os.path.splitext(vmx)[-1] == '.vmx': 200 | if os.path.exists(vmx): 201 | return True 202 | 203 | def main(): 204 | parser = argparse.ArgumentParser() 205 | parser.add_argument('--list', help='List monitored and running VMs', 206 | action='store_true') 207 | parser.add_argument('--monitor', help='Service to monitor VMs', action='store_true') 208 | parser.add_argument('--add', help='Add monitoring for /path/to/vmx') 209 | parser.add_argument('--remove', help='Remove monitoring for /path/to/vmx') 210 | parser.add_argument('--reset', help='Reset /path/to/vmx') 211 | parser.add_argument('--get-ip', help='Get IP for /path/to/vmx') 212 | args = parser.parse_args() 213 | 214 | syslog.openlog('vserv') 215 | 216 | monitor = VMXMonitor() 217 | 218 | if args.list: 219 | if not monitor.d.keys(): 220 | print 'No scheduled jobs' 221 | for k, v in monitor.get_scheduled().items(): 222 | print 'Monitoring: %s (%s, %s, %s)' % (v[0], v[2], v[3], k) 223 | for k, v in monitor.get_running().items(): 224 | print 'Running: %s (%s)' % (k, v) 225 | 226 | elif args.add: 227 | if not monitor.vmx_is_valid(args.add): 228 | print 'Invalid path (must be a vmx file): %s' % args.add 229 | sys.exit(1) 230 | if not monitor.is_scheduled(args.add): 231 | monitor.set_start(args.add) 232 | 233 | elif args.remove: 234 | if not monitor.vmx_is_valid(args.remove): 235 | print 'Invalid path (must be a vmx file): %s' % args.remove 236 | sys.exit(1) 237 | if monitor.is_scheduled(args.remove): 238 | monitor.set_stop(args.remove) 239 | 240 | elif args.reset: 241 | if not monitor.vmx_is_valid(args.reset): 242 | print 'Invalid path (must be a vmx file): %s' % args.reset 243 | sys.exit(1) 244 | if monitor.is_scheduled: 245 | monitor.set_reset(args.reset) 246 | 247 | elif args.get_ip: 248 | print monitor.get_ip(args.get_ip) 249 | 250 | elif args.monitor: 251 | while True: 252 | try: 253 | monitor = VMXMonitor() 254 | for vmx in monitor.get_monitored(): 255 | if monitor.is_scheduled(vmx): 256 | if not monitor.is_running(vmx): 257 | print 'Starting: %s' % vmx 258 | syslog.syslog(syslog.LOG_ALERT, 'Starting: %s' % vmx) 259 | monitor.start_vmx(vmx) 260 | monitor.set_running(vmx) 261 | elif monitor.to_be_removed(vmx): 262 | if monitor.is_running(vmx): 263 | print 'Stopping: %s' % vmx 264 | syslog.syslog(syslog.LOG_ALERT, 'Stopping: %s' % vmx) 265 | monitor.stop_vmx(vmx) 266 | monitor.remove(vmx) 267 | elif monitor.to_be_reset(vmx): 268 | if monitor.is_running(vmx): 269 | print 'Resetting: %s' % vmx 270 | syslog.syslog(syslog.LOG_ALERT, 'Resetting: %s' % vmx) 271 | monitor.reset_vmx(vmx) 272 | else: 273 | print 'Starting: %s' % vmx 274 | syslog.syslog(syslog.LOG_ALERT, 'Starting: %s' % vmx) 275 | monitor.start_vmx(vmx) 276 | monitor.set_running(vmx) 277 | time.sleep(15) 278 | except KeyboardInterrupt: 279 | print '...later!' 280 | sys.exit(1) 281 | 282 | else: 283 | parser.print_help() 284 | 285 | if __name__ == '__main__': 286 | main() --------------------------------------------------------------------------------