├── .gitignore ├── MANIFEST.in ├── CHANGES.txt ├── setup.py ├── LICENSE.txt ├── README.md └── vmfusion └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.md 3 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.2.0, 2014-09-25 -- Add optional vmrun_path parameter to vmrun_cli. 2 | v0.1.1, 2013-11-17 -- Implement __len__, __contains__, __iter__ for dhcpd_leases. 3 | v0.1.0, 2013-11-17 -- Initial release. 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='vmfusion', 5 | version='0.2.0', 6 | author='Mario Steinhoff', 7 | author_email='steinhoff.mario@gmail.com', 8 | packages=['vmfusion'], 9 | url='https://github.com/msteinhoff/vmfusion-python', 10 | license='LICENSE.txt', 11 | description='A python API for the VMware Fusion CLI tools.', 12 | long_description=open('README.md').read(), 13 | install_requires=[ 14 | "pyparsing >= 2.0.1" 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mario Steinhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vmfusion-python 2 | 3 | vmfusion-python is a low-level python interface for the VMware Fusion command 4 | line tools vmrun and vmware-vdiskmanager. It aims to be human readable (because 5 | I still have no idea why -k is shrink, -n is rename and -r is convert) and 6 | easy to use. 7 | 8 | The vmrun command comes with a ton of command line options. At the moment, only 9 | the common commands are implemented because I have yet to find an actual use 10 | case for the rest. If you have need for a non-implemented command, open an issue 11 | or send me a pull request. 12 | 13 | # Installation 14 | 15 | $ pip install vmfusion 16 | 17 | # Overview 18 | 19 | ## vmrun 20 | 21 | The `vmrun` tool can be used to control the runtime state of a VM. 22 | 23 | Note: Contrary to the behavior of vmrun, the API will work with both absolute 24 | _and_ relative VMX paths. 25 | 26 | ### list 27 | 28 | Return information about all running VMs. 29 | 30 | Usage: `vmrun.list()` 31 | 32 | Example: 33 | 34 | >>> from vmfusion import vmrun 35 | >>> print vmrun.list() 36 | { 37 | 'count': 3, 38 | 'machines': [ 39 | '/Users/msteinhoff/Documents/Virtual Machines/test1/test1.vmx', 40 | '/Users/msteinhoff/Documents/Virtual Machines/test1/test2.vmx', 41 | '/Users/Shared/Virtual Machines/test3/test3.vmx', 42 | ] 43 | } 44 | 45 | *Note:* This will only return actual running VMs and is not the same als the 46 | Virtual Machine Library from the VMware Fusion GUI, which also displays halted 47 | and suspended VMs. 48 | 49 | ### start 50 | 51 | Power on a VM. 52 | 53 | Usage: 54 | 55 | vmrun.start( vmx ) 56 | 57 | **Note/Warning:** There is an optional `gui` parameter which can be set to False 58 | to launch the VM in *nogui mode*. 59 | 60 | The nogui mode is weird. In nogui mode, the VM window will not be visible but 61 | displayed in VMware Fusion.app when its already running. If the Fusion GUI is 62 | not running and you launch it or close/launch it, the VM gets converted back to 63 | gui mode. Oh and if you then close VMware Fusion.app the VM gets suspended. (at 64 | least thats whats happening on my system). 65 | 66 | ### stop 67 | 68 | Shutdown a VM. 69 | 70 | Usage: 71 | 72 | vmrun.stop( vmx, soft=True ) 73 | 74 | When `soft` is set to True (default), configured shutdown scripts will be 75 | executed and an shutdown signal will be sent to the guest OS. For this to work, 76 | vmware tools need to be installed. Otherwise, the VM is killed with no mercy. 77 | 78 | ### reset 79 | 80 | Reboot a VM. 81 | 82 | Usage: 83 | 84 | vmrun.reset( vmx, soft=True ) 85 | 86 | When `soft` is set to True (default), configured shutdown/power-on scripts will 87 | be executed and an shutdown signal will be sent to the guest OS. For this to 88 | work, vmware tools need to be installed. Otherwise, the VM is killed with no 89 | mercy. 90 | 91 | ### suspend 92 | 93 | Suspend VM state to disk. 94 | 95 | Usage: 96 | 97 | vmrun.suspend( vmx, soft=True ) 98 | 99 | When `soft` is set to True (default), configured suspend scripts will be 100 | executed before the system is suspended. 101 | 102 | ### pause 103 | 104 | Halt the CPU execution of a VM. 105 | 106 | Usage: 107 | 108 | vmrun.pause( vmx ) 109 | 110 | ### unpause 111 | 112 | Continue CPU execution of a M. 113 | 114 | Usage: 115 | 116 | vmrun.unpause( vmx ) 117 | 118 | ### list_snapshots 119 | 120 | List the snapshots in a VM. 121 | 122 | Usage: 123 | 124 | vmrun.list_snapshots( vmx ) 125 | 126 | ### snapshot 127 | 128 | Take a new snapshot of the VM. 129 | 130 | Usage: 131 | 132 | vmrun.snapshot( vmx, name ) 133 | 134 | ### revert\_to\_snapshot 135 | 136 | Revert the VM to a snapshot. 137 | 138 | Usage: 139 | 140 | vmrun.revert_to_snapshot( vmx, name ) 141 | 142 | ### delete_snapshot 143 | 144 | Delete a snapshot. 145 | 146 | Usage: 147 | 148 | vmrun.delete_snapshot( vmx, name ) 149 | 150 | 151 | ## VM 152 | 153 | The VM class represents a virtual machine, and is used as a convenient wrapper 154 | for the `vmrun` commands to operate on a common VM. The same functions that 155 | `vmrun` provides that take a `vmx` parameter are exposed through this class. 156 | 157 | Usage: 158 | 159 | vm = VM('/path/to/virtual_machine.vmx') 160 | vm.start() 161 | vm.suspend() 162 | vm.stop() 163 | vm.snapshot('some snapshot') 164 | vm.revert_to_snapshot('some snapshot') 165 | vm.delete_snapshot('some snapshot') 166 | 167 | 168 | ## vdiskmanager 169 | 170 | With the `vdiskmanager` tool VMDK disks can be managed. For all methods, the `vmdk` parameter always expects a relative path to the vmdk file. 171 | 172 | ### create 173 | 174 | Creates a new VMDK file with the given parameters. 175 | 176 | Usage: 177 | 178 | vdiskmanager.create( vmdk, size, disk_type=None, adapter_type=None ) 179 | 180 | Arguments: 181 | 182 | - `size` 183 | 184 | A size specification readable by the tool, e.g. `100MB`, `20GB`, `1TB`. No 185 | validation is performed. 186 | 187 | - `disk_type` 188 | 189 | Optional type of the disk to be created, one of the following: 190 | 191 | - `SPARSE_SINGLE`: A single growable VMDK file 192 | - `SPARSE_SPLIT`: Many growable VMDK files, split into 2 GB slices 193 | - `PREALLOC_SINGLE`: A single preallocated VMDK file 194 | - `PREALLOC_SPLIT`: Many preallocated 2 GB VMDK files 195 | 196 | Default is `SPARSE_SPLIT`. 197 | 198 | - `adapter_type` 199 | 200 | Optional type of the disk adapter, one of the following: 201 | 202 | - `IDE` 203 | - `LSILOGIC` 204 | - `BUSLOGIC` 205 | 206 | Default is `LSILOGIC`. 207 | 208 | ### defragment 209 | 210 | Defragments VMDK files on the VMware level (not to be confused with guest 211 | filesystem defagmentation). 212 | 213 | Usage: 214 | 215 | vdiskmanager.defragment( vmdk ) 216 | 217 | ### shrink 218 | 219 | This will perform a shrink of the VMDK on the VMware level (The guest 220 | filesystem must be prepared for this to work, e.g. with the zerofill tool on 221 | Linux.). 222 | 223 | Usage: 224 | 225 | vdiskmanager.shrink( vmdk ) 226 | 227 | ### rename 228 | 229 | This will rename a VMDK file. Useful for large split disks with over 9000 2GB 230 | slices. 231 | 232 | Usage: 233 | 234 | vdiskmanager.rename( source_vmdk, destination_vmdk ) 235 | 236 | ### convert 237 | 238 | This will convert the disk type of the given VMDK file. 239 | 240 | Usage: 241 | 242 | vdiskmanager.convert( vmdk, disk_type ) 243 | 244 | The `disk_type` parameter is the same as in create() and must be one of the 245 | following: 246 | 247 | - `SPARSE_SINGLE`: A single growable VMDK file 248 | - `SPARSE_SPLIT`: Many growable VMDK files, split into 2 GB slices 249 | - `PREALLOC_SINGLE`: A single preallocated VMDK file 250 | - `PREALLOC_SPLIT`: Many preallocated 2 GB VMDK files 251 | 252 | ### expand 253 | 254 | This will expand the VMDK to the given size. 255 | 256 | Usage: 257 | 258 | vdiskmanager.convert( vmdk, new_size ) 259 | 260 | The `new_size` parameter must be a size specification readable by the tool, e.g. 261 | `100MB`, `20GB`, `1TB`. No validation is performed. 262 | 263 | # vmnet_* 264 | 265 | It is often handy to gather certain information about the local VMware networks. 266 | 267 | By default VMware creates a host-only network and a NAT network. Those are 268 | represented by `vmnet_hostonly` and `vmnet_nat`. 269 | 270 | To retrieve the vnet-name (e.g. useful in VMX config files), use the following: 271 | 272 | >>> from vmfusion import vmnet_nat 273 | >>> print vmnet_nat.name 274 | vmnet8 275 | 276 | There is a DHCP server running on both networks. To access the lease information, 277 | use the following: 278 | 279 | >>> from vmfusion import vmnet_nat 280 | >>> print vmnet_nat.leases 281 | { 282 | '00:50:56:00:23:40': '192.168.128.130' 283 | '00:50:56:00:19:12': '192.168.128.129' 284 | '00:50:56:00:46:93': '192.168.128.136' 285 | } 286 | 287 | The dhcp server stores lease information in a file on disk. The data in the 288 | leases dictionary is read-only and not automatically updated. To reload the 289 | latest data from the file, use the `reload()` method: 290 | 291 | vmnet_nat.leases.reload() 292 | 293 | # Custom tool locations 294 | 295 | The `vmrun` and `vdiskmanager` use the default location at `/Applications/VMware Fusion.app`. This can be changed by instantiating a custom _cli object. 296 | 297 | vmrun at a custom location: 298 | 299 | >>> from vmfusion import vmrun_cli 300 | >>> vmrun_custom = vmrun_cli( '/Volumes/External/Applications/VMware Fusion.app' ) 301 | 302 | Same goes with vdiskmanager: 303 | 304 | >>> from vmfusion import vdiskmanager_cli 305 | >>> vdiskmanager_custom = vdiskmanager_cli( '/Volumes/External/Applications/VMware Fusion.app' ) 306 | 307 | To create a custom vmnet, use `vnet_cli`: 308 | 309 | >>> from vmfusion import vnet_cli 310 | >>> vmnet_custom = vnet_cli( 'vmnet6' ) 311 | >>> print vmnet_custom.name 312 | vmnet6 313 | 314 | # Contribution 315 | 316 | Fork, code, pull request :) 317 | 318 | 319 | # License 320 | 321 | See LICENSE.txt 322 | -------------------------------------------------------------------------------- /vmfusion/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from datetime import datetime, timedelta 4 | from pyparsing import * 5 | 6 | def file_must_exist( type, file ): 7 | if not os.path.isfile( file ): 8 | raise ValueError( "{0} at path {1} does not exist".format( type, file ) ) 9 | 10 | def file_must_not_exist( type, file ): 11 | if os.path.isfile( file ): 12 | raise ValueError( "{0} at path {1} already exists".format( type, file ) ) 13 | 14 | def get_abspath( path ): 15 | if not os.path.isabs( path ): 16 | path = os.path.abspath( path ) 17 | 18 | return path 19 | 20 | 21 | class VMRunException(Exception): pass 22 | 23 | 24 | class vmrun_cli( object ): 25 | """Human readable python interface to the vmrun cli tool of VMware Fusion. 26 | 27 | Tested with VMware Fusion 5.""" 28 | 29 | def __init__( self, bundle_directory=None, vmrun_path=None ): 30 | if not vmrun_path: 31 | if not bundle_directory: 32 | bundle_directory = '/Applications/VMware Fusion.app' 33 | 34 | vmrun = os.path.join( bundle_directory, 'Contents/Library/vmrun' ) 35 | else: 36 | vmrun = vmrun_path 37 | 38 | if not os.path.isfile( vmrun ): 39 | raise ValueError( "vmrun tool not found at path {0}".format( vmrun ) ) 40 | 41 | self.tool_path = vmrun 42 | 43 | def __vmrun( self, command ): 44 | base = [ self.tool_path, '-T', 'fusion' ] 45 | base.extend( command ) 46 | 47 | proc = subprocess.Popen( base, stdout=subprocess.PIPE ) 48 | stdout = proc.stdout.readlines() 49 | 50 | if len(stdout) and stdout[0].startswith('Error'): 51 | raise VMRunException(stdout[0]) 52 | 53 | return stdout 54 | 55 | def list( self ): 56 | output = self.__vmrun( [ 'list' ] ) 57 | 58 | # Expected output: 59 | # Total running VMs: N 60 | # [optional absolute path to VMX 1] 61 | # [optional absolute path to VMX 2] 62 | # [optional absolute path to VMX n] 63 | data = {} 64 | data[ 'count' ] = int(output[0].split(':')[1].strip()) 65 | data[ 'machines' ] = [vmx.strip() for vmx in output[1:]] 66 | 67 | return data 68 | 69 | def start( self, vmx, gui=True ): 70 | vmx = get_abspath( vmx ) 71 | 72 | file_must_exist( 'VMX', vmx ) 73 | 74 | gui_value = ( 'nogui', 'gui' )[gui] 75 | self.__vmrun( [ 'start', vmx, gui_value ] ) 76 | 77 | def stop( self, vmx, soft=True ): 78 | vmx = get_abspath( vmx ) 79 | 80 | file_must_exist( 'VMX', vmx ) 81 | 82 | soft_value = ( 'hard', 'soft' )[soft] 83 | self.__vmrun( [ 'stop', vmx, soft_value ] ) 84 | 85 | def reset( self, vmx, soft=True ): 86 | vmx = get_abspath( vmx ) 87 | 88 | file_must_exist( 'VMX', vmx ) 89 | 90 | soft_value = ( 'hard', 'soft' )[soft] 91 | self.__vmrun( [ 'reset', vmx, soft_value ] ) 92 | 93 | def suspend( self, vmx, soft=True ): 94 | vmx = get_abspath( vmx ) 95 | 96 | file_must_exist( 'VMX', vmx ) 97 | 98 | soft_value = ( 'hard', 'soft' )[soft] 99 | self.__vmrun( [ 'suspend', vmx, soft_value ] ) 100 | 101 | def pause( self, vmx ): 102 | vmx = get_abspath( vmx ) 103 | 104 | file_must_exist( 'VMX', vmx ) 105 | 106 | self.__vmrun( [ 'pause', vmx ] ) 107 | 108 | def unpause( self, vmx ): 109 | vmx = get_abspath( vmx ) 110 | 111 | file_must_exist( 'VMX', vmx ) 112 | 113 | self.__vmrun( [ 'unpause', vmx ] ) 114 | 115 | def delete( self, vmx ): 116 | vmx = get_abspath( vmx ) 117 | 118 | file_must_exist( 'VMX', vmx ) 119 | 120 | self.__vmrun( [ 'delete', vmx ] ) 121 | 122 | def list_snapshots( self, vmx ): 123 | vmx = get_abspath( vmx ) 124 | 125 | file_must_exist( 'VMX', vmx ) 126 | 127 | output = self.__vmrun( [ 'listSnapshots', vmx ] ) 128 | snapshots = [s.strip() for s in output[1:]] 129 | data = {'count': len(snapshots), 'snapshots': snapshots} 130 | 131 | return data 132 | 133 | def snapshot( self, vmx, name ): 134 | vmx = get_abspath( vmx ) 135 | 136 | file_must_exist( 'VMX', vmx ) 137 | 138 | self.__vmrun( [ 'snapshot', vmx, name ] ) 139 | 140 | def revert_to_snapshot( self, vmx, name ): 141 | vmx = get_abspath( vmx ) 142 | 143 | file_must_exist( 'VMX', vmx ) 144 | 145 | self.__vmrun( [ 'revertToSnapshot', vmx, name ] ) 146 | 147 | def delete_snapshot( self, vmx, name ): 148 | vmx = get_abspath( vmx ) 149 | 150 | file_must_exist( 'VMX', vmx ) 151 | 152 | self.__vmrun( [ 'deleteSnapshot', vmx, name ] ) 153 | 154 | 155 | class VM(object): 156 | """ 157 | A virtual machine. 158 | """ 159 | def __init__(self, vmx): 160 | self.vmx = get_abspath(vmx) 161 | file_must_exist('VMX', self.vmx) 162 | self.vmrun = vmrun_cli() 163 | 164 | def start(self, gui=True): 165 | return self.vmrun.start(self.vmx, gui) 166 | 167 | def stop(self, soft=True): 168 | return self.vmrun.stop(self.vmx, soft) 169 | 170 | def reset(self, soft=True): 171 | return self.vmrun.reset(self.vmx, soft) 172 | 173 | def suspend(self, soft=True): 174 | return self.vmrun.suspend(self.vmx, soft) 175 | 176 | def pause(self): 177 | return self.vmrun.pause(self.vmx) 178 | 179 | def unpause(self): 180 | return self.vmrun.unpause(self.vmx) 181 | 182 | def delete(self): 183 | return self.vmrun.delete(self.vmx) 184 | 185 | def list_snapshots(self): 186 | return self.vmrun.list_snapshots(self.vmx) 187 | 188 | def snapshot(self, name): 189 | return self.vmrun.snapshot(self.vmx, name) 190 | 191 | def revert_to_snapshot(self, name): 192 | return self.vmrun.revert_to_snapshot(self.vmx, name) 193 | 194 | def delete_snapshot(self, name): 195 | return self.vmrun.delete_snapshot(self.vmx, name) 196 | 197 | 198 | class vdiskmanager_cli( object ): 199 | # Valid disks 200 | SPARSE_SINGLE = 'SPARSE_SINGLE' 201 | SPARSE_SPLIT = 'SPARSE_SPLIT' 202 | PREALLOC_SINGLE = 'PREALLOC_SINGLE' 203 | PREALLOC_SPLIT = 'PREALLOC_SPLIT' 204 | disk_type_map = { 205 | 'SPARSE_SINGLE': '0', 206 | 'SPARSE_SPLIT': '1', 207 | 'PREALLOC_SINGLE': '2', 208 | 'PREALLOC_SPLIT': '3' 209 | } 210 | 211 | # Valid adapters 212 | IDE = 'ide' 213 | LSILOGIC = 'lsilogic' 214 | BUSLOGIC = 'buslogic' 215 | adapters = [ IDE, LSILOGIC, BUSLOGIC ] 216 | 217 | """Human readable python interface to the vmware-vdiskmanager cli of VMware Fusion. 218 | 219 | Tested with VMware Fusion 5.""" 220 | 221 | def __init__( self, bundle_directory=None ): 222 | if not bundle_directory: 223 | bundle_directory = '/Applications/VMware Fusion.app' 224 | 225 | vmrun = os.path.join( bundle_directory, 'Contents/Library/vmware-vdiskmanager' ) 226 | 227 | if not os.path.isfile( vmrun ): 228 | raise ValueError( "vmrun tool not found at path {0}".format( vmrun ) ) 229 | 230 | self.tool_path = vmrun 231 | 232 | def __vdiskmanager( self, command ): 233 | base = [ self.tool_path ] 234 | base.extend( command ) 235 | 236 | proc = subprocess.call( base ) 237 | 238 | def create( self, vmdk, size, disk_type=None, adapter_type=None ): 239 | file_must_not_exist( 'VMDK', vmdk ) 240 | 241 | # disk type 242 | if not disk_type: 243 | disk_type = self.SPARSE_SPLIT 244 | 245 | if disk_type not in self.disk_type_map: 246 | raise ValueError( "Invalid disk type {0}".format( disk_type ) ) 247 | 248 | # adapter type 249 | if not adapter_type: 250 | adapter_type = self.LSILOGIC 251 | 252 | if adapter_type not in self.adapters: 253 | raise ValueError( "Invalid adapter type {0}".format( adapter_type ) ) 254 | 255 | self.__vdiskmanager( [ '-c', '-s', size, '-a', adapter_type, '-t', self.disk_type_map[ disk_type ], vmdk ] ) 256 | 257 | def defragment( self, vmdk ): 258 | file_must_exist( 'VMDK', vmdk ) 259 | 260 | self.__vdiskmanager( [ '-d', vmdk ] ) 261 | 262 | def shrink( self, vmdk ): 263 | file_must_exist( 'VMDK', vmdk ) 264 | 265 | self.__vdiskmanager( [ '-k', vmdk ] ) 266 | 267 | def rename( self, source_vmdk, destination_vmdk ): 268 | file_must_exist( 'VMDK', source_vmdk ) 269 | file_must_not_exist( 'VMDK', destination_vmdk ) 270 | 271 | self.__vdiskmanager( [ '-n', source_vmdk, destination_vmdk ] ) 272 | 273 | def convert( self, vmdk, disk_type ): 274 | file_must_exist( 'VMDK', vmdk ) 275 | 276 | if disk_type not in self.disk_type_map: 277 | raise ValueError( "Invalid disk type {0}".format( disk_type ) ) 278 | 279 | self.__vdiskmanager( [ '-r', '-t', self.disk_type_map[ disk_type ], vmdk ] ) 280 | 281 | def expand( self, vmdk, new_size ): 282 | file_must_exist( 'VMDK', vmdk ) 283 | 284 | self.__vdiskmanager( [ '-x', size, vmdk ] ) 285 | 286 | class dhcpd_leases( object ): 287 | """A dhcpd_leases contains a mapping between MAC and IP addresses from the 288 | content of a given dhpcd.leases file. 289 | 290 | When the lease file contains multiple leases for the same mac address, the 291 | lease with the latest start date is used.""" 292 | 293 | def __init__( self, lease_file ): 294 | if not os.path.isfile( lease_file ): 295 | raise ValueError( "dhcpd.leases '{0}' not found".format( lease_file ) ) 296 | 297 | self.lease_file = lease_file 298 | self.list = {} 299 | 300 | def __parse( self ): 301 | LBRACE, RBRACE, SEMI, QUOTE = map(Suppress,'{};"') 302 | ipAddress = Combine(Word(nums) + ('.' + Word(nums))*3) 303 | hexint = Word(hexnums,exact=2) 304 | macAddress = Combine(hexint + (':'+hexint)*5) 305 | hdwType = Word(alphanums) 306 | yyyymmdd = Combine((Word(nums,exact=4)|Word(nums,exact=2))+('/'+Word(nums,exact=2))*2) 307 | hhmmss = Combine(Word(nums,exact=2)+(':'+Word(nums,exact=2))*2) 308 | dateRef = oneOf(list("0123456"))("weekday") + yyyymmdd("date") + hhmmss("time") 309 | def to_datetime(tokens): 310 | tokens["datetime"] = datetime.strptime("%(date)s %(time)s" % tokens, "%Y/%m/%d %H:%M:%S") 311 | dateRef.setParseAction(to_datetime) 312 | startsStmt = "starts" + dateRef + SEMI 313 | endsStmt = "ends" + (dateRef | "never") + SEMI 314 | tstpStmt = "tstp" + dateRef + SEMI 315 | tsfpStmt = "tsfp" + dateRef + SEMI 316 | hdwStmt = "hardware" + hdwType("type") + macAddress("mac") + SEMI 317 | uidStmt = "uid" + QuotedString('"')("uid") + SEMI 318 | bindingStmt = "binding" + Word(alphanums) + Word(alphanums) + SEMI 319 | leaseStatement = startsStmt | endsStmt | tstpStmt | tsfpStmt | hdwStmt | uidStmt | bindingStmt 320 | leaseDef = "lease" + ipAddress("ipaddress") + LBRACE + Dict(ZeroOrMore(Group(leaseStatement))) + RBRACE 321 | 322 | with open( self.lease_file, 'r' ) as file: 323 | 324 | parsed = leaseDef.scanString( file.read() ) 325 | 326 | return parsed 327 | 328 | def load( self ): 329 | all_leases = {} 330 | 331 | for lease, start, stop in self.__parse(): 332 | try: 333 | mac = lease.hardware.mac 334 | 335 | if mac not in all_leases or lease.starts.datetime > all_leases[mac].starts.datetime: 336 | all_leases[ mac ] = lease 337 | 338 | except AttributeError, e: 339 | print e 340 | 341 | for mac in all_leases: 342 | all_leases[ mac ] = all_leases[ mac ].ipaddress 343 | 344 | self.list = all_leases 345 | 346 | reload = load 347 | 348 | def __len__( self ): 349 | return len( self.list ) 350 | 351 | def __iter__( self ): 352 | return self.list.iterkeys() 353 | 354 | def __getitem__( self, item ): 355 | return self.list[ item ] 356 | 357 | def __contains__( self, item ): 358 | return item in self.list 359 | 360 | def __str__( self ): 361 | return str( self.list ) 362 | 363 | def __repr__( self ): 364 | return repr( self.list ) 365 | 366 | class vnet_cli(object): 367 | def __init__( self, name ): 368 | self.name = name 369 | self._parse_networking() 370 | self._load_dhcp_leases() 371 | 372 | def _load_dhcp_leases(self): 373 | try: 374 | path = '/var/db/vmware/vmnet-dhcpd-{}.leases' 375 | self.leases = dhcpd_leases( path.format(self.name) ) 376 | self.leases.load() 377 | except ValueError: 378 | self.leases = None 379 | 380 | def _parse_networking(self): 381 | netfile = "/Library/Preferences/VMware Fusion/networking" 382 | net_num = self.name[-1] 383 | net_name = "VNET_{0}".format(net_num) 384 | match = re.compile("answer\s+{0}_(\w+)\s+(.*)$".format(net_name)).match 385 | attrs = {} 386 | 387 | with open(netfile) as net: 388 | content = net.read() 389 | if net_name not in content: 390 | msg = "No network named {0} is defined!" 391 | raise ValueError(msg.format(self.name)) 392 | 393 | for line in content.split("\n"): 394 | m = match(line) 395 | if m: 396 | attr = m.group(1).lower() 397 | val = m.group(2) 398 | if val == 'yes': 399 | val = True 400 | elif val == 'no': 401 | val = False 402 | attrs[attr] = val 403 | 404 | self.dhcp = attrs.get("dhcp", False) 405 | self.netmask = attrs.get("hostonly_netmask", None) 406 | self.subnet = attrs.get("hostonly_subnet", None) 407 | self.nat = attrs.get("nat", False) 408 | self.virtual_adapter = attrs.get("virtual_adapter", False) 409 | 410 | 411 | # Default access 412 | vmrun = vmrun_cli() 413 | vdiskmanager = vdiskmanager_cli() 414 | vmnet_hostonly = vnet_cli( 'vmnet1' ) 415 | vmnet_nat = vnet_cli( 'vmnet8' ) 416 | --------------------------------------------------------------------------------