forked from rapid7/vm-automation
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathesxiSsh.py
executable file
·288 lines (234 loc) · 11.9 KB
/
esxiSsh.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# Created by Aaron Soto (@_surefire_)
# in collaboration with Brendan Watters (@tychos_moose)
# released under the BSD license
# v0.1 (2017-June-25)
# TODO: Test on ESXi 6.0 and 5.5 (only tested with ESXi 6.5)
# TODO: Confirm that snapshots work
# TODO: Determine whether VMX hardware changes between snapshots are copied over
# TODO: Allow cloining from a previous source snapshot to a new destination machine
import esxiVm
import pexpect
import sys
from pyVmomi import vim
class esxiSsh(esxiVm.esxiServer):
def copyOvfTool(self):
# TODO: Detect if this version of ovftool is already present on remote server, then return true
ovfToolsPath = './ovftool-4.2.0-4586971.tgz'
ovfToolsURL = 'https://my.vmware.com/group/vmware/details?downloadGroup=OVFTOOL420&productId=491'
try:
f = open(ovfToolsPath,"rb");
f.close()
except IOError:
print "FATAL ERROR: VMware 'ovftool' is required for server-to-server cloning. Download it from VMware at:"
print " " + ovfToolsURL
print "and place it in ovfToolsPath: " + ovfToolsPath
sys.exit(-1)
session = pexpect.spawn('scp -o ConnectTimeout=5 ' + ovfToolsPath + ' ' + \
self.username + '@' + self.hostname + ":/tmp/ovftool.tgz")
i = session.expect(['Are you sure you want to continue connecting (yes/no)?', 'Password:', 'Connection refused', 'Connection timed out'])
if i == 0:
# Are you sure you want to continue connecting (yes/no)?
session.sendline('yes')
session.expect('Password:')
session.sendline(self.password)
if i == 1:
# Password:
session.sendline(self.password)
if i == 2:
self.fatalError("Connection refused. Confirm that SSH is enabled on the ESXi server")
# Connection refused
if i == 3:
self.fatalError("Connection refused. Confirm the IP address and that SSH is permitted on host.")
# Connection timed out
session.expect('100%')
return True
def cloneToServer(self, srcVm, destServer, destDatastore, destVm, timeout=60*30):
# Copying between servers takes a while, so the default timeout is 30 minutes.
if type(srcVm) != str or type(destVm) != str:
fatalError("Source and destination VMs must be the VM names as strings.")
elif type(destDatastore) != str:
fatalError("Destination datastore must be a string.")
elif type(destServer) != str:
fatalError("Destination datastore must be the IP address as a string.")
path, srcVmdk = self.findVmdkPath(srcVm)
session = self.loginToEsx()
# Configure source server firewall to allow outbound SSH and HTTP
self.toggleFirewallRules(session,enabled=True)
# Deploy OVF Tool on source server
self.deployOvfTool(session)
srcServer = self.username + ":" + self.password + "@" + self.hostname
session.sendline('/tmp/ovftool/ovftool -dm=thin -ds=' + destDatastore \
+ ' --name=' + destVm + ' vi://' + srcServer + '/' + srcVm \
+ ' vi://' + destServer)
# TODO: Fill in the following for success, timeout/refused, and name already exists
i = session.expect(['Completed successfully','Error: Internal error: Failed to connect to server','Error: Duplicate name','Invalid target datastore specified','No network mapping specified'],timeout=timeout)
if i == 0:
# Completed successfully
return True
elif i == 1:
# Error: Internal error: Failed to connect to server
# (Note: This occurs when there's a timeout or connection refused. No way to discern the difference.)
self.fatalError("Unable to connect to destination server. Connection timed out or refused.")
elif i == 2:
# Error: Duplicate name
self.fatalError("VM name already exists on destination server")
elif i == 3:
# Invalid target datastore specified
self.fatalError("Datastore name not found on destination server")
elif i == 4:
# No network mapping specified.
self.fatalError("Destination server is missing the case-sensitive network name required by this VM.")
session.interact()
def deployOvfTool(self,session):
# Clear any previous files/directories
session.sendline('rm -rf /tmp/ovftool*')
# SCP the OVF tool to the source server
self.copyOvfTool()
# Deploy the OVF tool
session.sendline('mkdir /tmp/ovftool')
session.sendline('tar xf /tmp/ovftool.tgz -C /tmp/ovftool')
# Confirm OVF tool deployed properly
session.sendline('/tmp/ovftool/ovftool --version')
session.expect('VMware ovftool',timeout=5)
def toggleFirewallRules(self,session,enabled=False):
prompt = ':~]'
session.expect(prompt)
for service in ['sshClient','httpClient']:
session.sendline('esxcli network firewall ruleset set -e ' + str(enabled).lower() + \
' -r ' + service)
session.expect(prompt)
def clone(self, srcVm, destVm, thinProvision=True):
# srcVm = string, name of source VM
# destVm = string, name of destination VM
# Limitations:
# Source VM must exist on one datastore
# Source VM name must be unique
# esxiServer must not be a vCenter server
# VM cannot contain multiple disks
if type(srcVm) != str or type(destVm) != str:
fatalError("Source and destination VMs must be the VM names as strings.")
path, srcVmdk = self.findVmdkPath(srcVm)
session = self.loginToEsx()
# Make destination directory
session.sendline('cd ' + path)
session.sendline('mkdir ' + destVm)
session.sendline('ls ' + path + '/' + destVm)
# Copy non-VMDK files from source VM to destination VM
session.sendline('find "' + path + '/' + srcVm + '" -maxdepth 1 -type f | grep -v ".vmdk"' + \
' | while read file; do cp "$file" "' + path + '/' + destVm + '"; done')
# Copy VMDK files from source VM to destination VM
destVmdk = srcVmdk.split('/')[-1]
if thinProvision:
session.sendline('vmkfstools -i "' + path + '/' + srcVmdk + '" -d thin "' \
+ path + '/' + destVm + '/' + destVmdk + '"')
else:
session.sendline('vmkfstools -i "' + path + '/' + srcVmdk + '" -d zeroedthick "' \
+ path + '/' + destVm + '/' + destVmdk + '"')
# Wait for the VMDK copying to complete, and check for errors
while True:
i = session.expect(["Clone: 100% done.","Failed to clone disk","Failed to lock the file"], timeout=60*30)
if i == 0:
break
elif i == 1:
try:
session.expect("The file already exists", timeout=1)
self.fatalError("The VMDK already exists. Pick a different destination VM name, or clean up your datastore first.")
return False
except pexpect.TIMEOUT:
self.fatalError("An unknown error occured copying the VMDK. Here, have a shell:")
session.interact()
elif i == 2:
self.fatalError("Unable to lock the VMDK file. The VM must be powered off.")
return False
# One last thing, register the new VM in the ESXi inventory
session.sendline('vim-cmd solo/registervm ' + path + '/' + destVm + '/' + srcVm + '.vmx ' \
+ destVm)
try:
i = session.expect(['^[0-9]+$'], timeout=30)
if i == 0:
print session.before
print session.after
else:
print "???"
return True
except pexpect.TIMEOUT:
return False
session.sendline("exit")
session.expect(["Connection to .* closed."],timeout=10)
def loginToEsx(self):
session = pexpect.spawn('ssh -o ConnectTimeout=5 ' + self.username + '@' + self.hostname)
i = session.expect(['Are you sure you want to continue connecting (yes/no)?', 'Password:', 'Connection refused', 'Connection timed out'])
if i == 0:
# Are you sure you want to continue connecting (yes/no)?
session.sendline('yes')
session.expect('Password:')
session.sendline(self.password)
if i == 1:
# Password:
session.sendline(self.password)
if i == 2:
self.fatalError("Connection refused. Confirm that SSH is enabled on the ESXi server")
# Connection refused
if i == 3:
self.fatalError("Connection refused. Confirm the IP address and that SSH is permitted on hte ESXi firewall")
# Connection timed out
return session
def findVmdkPath(self, srcVm):
# srcVm could be a name (str), a vmId (int), or a vm object (vmObject)
# destVm must be a name (what about workstation? where will I store the new VM?)
# what about ESXi? What datastore should I use?
self.enumerateVms()
if type(srcVm) == str:
datastore, srcVmdk = self.findVmdkByName(srcVm)
if not datastore or not srcVmdk:
self.fatalError("Source VMDK could not be located")
path = self.findDatastorePath(datastore)
return path, srcVmdk
def findVmdkByName(self,srcVm):
vmObject = None
for vm in self.vmList:
if vm.vmName == srcVm and vmObject == None:
vmObject = vm.vmObject
elif vm.vmName == srcVm:
self.fatalError("Unable to identify source VM. Multiple VMs have that name")
if vmObject == None:
self.fatalError("Unable to identify source VM. No VM found with that name")
if len(vmObject.config.datastoreUrl) > 1:
self.fatalError("VM uses multiple datastores")
src = None
path = vmObject.config.datastoreUrl[0].url
for device in vmObject.config.hardware.device:
if str(type(device)) == "<class 'pyVmomi.VmomiSupport.vim.vm.device.VirtualDisk'>":
if src == None:
src = device.backing.fileName
else:
self.fatalError("VM has multiple disks")
(datastore,srcVmdk) = src.split(" ")
datastore = datastore[1:-1]
return datastore,srcVmdk
def findDatastorePath(self,datastoreName):
content = self.connection.content
objView = content.viewManager.CreateContainerView(content.rootFolder,
[vim.HostSystem],
True)
view = objView.view
objView.Destroy()
if len(view) > 1:
self.fatalError("Multiple ESXi hosts found. You must connect to the ESXi server directly, not vCenter")
datastores = view[0].configManager.storageSystem.fileSystemVolumeInfo.mountInfo
for datastore in datastores:
if datastore.volume.type == "VMFS" and datastore.volume.name == datastoreName:
return datastore.mountInfo.path
def fatalError(self,str):
print "FATAL ERROR:", str
sys.exit(-1)
##########
# Example usage:
##########
# REQUIRED: First, connect to the ESXi server with the source image
# myserver = esxiSsh("192.168.1.1", "root", "password", "443", "esxi-192-168-1-1.log")
# myserver.connect()
# Copy a VM locally within myserver
# myserver.clone("sourceVmName","destinationVmName")
# Copy a VM from myserver to 192.168.1.2
# myserver.cloneToServer("sourceVmName","root:[email protected]","destinationDatastore","destinationVmName")