Merge lp:~mpontillo/maas/vmware-fixes--bug-1515188 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 5148
Proposed branch: lp:~mpontillo/maas/vmware-fixes--bug-1515188
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 276 lines (+105/-37)
2 files modified
src/provisioningserver/drivers/hardware/tests/test_vmware.py (+12/-1)
src/provisioningserver/drivers/hardware/vmware.py (+93/-36)
To merge this branch: bzr merge lp:~mpontillo/maas/vmware-fixes--bug-1515188
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+298727@code.launchpad.net

Commit message

Add workaround to allow an unverified SSL context on newer versions of pyvmomi. Fix _get_vm_list() to properly recurse folders.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Tested this end-to-end on Ante's VMware setup since I no longer have any VMware running in my own test bed. It solves the issue, when you add the chassis like this:

maas profile machines add-chassis chassis_type=vmware username=root password='xxxx' protocol='https+unverified' hostname=1.2.3.4 prefix_filter=maas

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Also, in order to get this to work, you need to do something like this, since the workaround only works on newer versions of the pyvmomi library:

$ sudo apt install python3-pip
$ sudo pip3 install --system --upgrade pyvmomi
$ sudo maas-rackd restart

Revision history for this message
Gavin Panella (allenap) wrote :

Looks good.

> Also, in order to get this to work, you need to do something like
> this, since the workaround only works on newer versions of the pyvmomi
> library: ...

Is there an SRU planned for this? We can't release this branch unless
there's a packaged version of this newer version of pyvmomi. It may be
prudent to not _land_ this branch until that dependency is in place.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the review. Yes, some of the abstract class stuff in the vmware driver is werid, and even unnecessary. That started out as a way to manually mock the entire pyvmomi API (what we used of it anyway), but it evolved into a bit of a mess when we actually ended up using mock.patch instead for some of the testing. Which is why you don't see a 2nd place I needed to override those abstract methods. (I didn't want to change too much of this code though, since this needs to be ported back to the RC - or I might have cleaned up more.) I'll quickly look into fixing it to use ABCMeta though, since you pointed it out.

It is unlikely that we can SRU a newer version of the library. We'll need to place it into a PPA (Or a workaround like "sudo pip3 install --system --upgrade pyvmomi" can be used, at your own risk.)

Since the user must specify "https+unverified" as the protocol, which is rather difficult to do (you must use the API), it's unlikely that someone will ever accidentally hit this scenario.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/drivers/hardware/tests/test_vmware.py'
2--- src/provisioningserver/drivers/hardware/tests/test_vmware.py 2016-03-28 13:54:47 +0000
3+++ src/provisioningserver/drivers/hardware/tests/test_vmware.py 2016-06-30 17:59:16 +0000
4@@ -209,6 +209,18 @@
5 mock_vmomi_api.SmartConnect.return_value = FakeVmomiServiceInstance(
6 servers=servers, has_instance_uuid=has_instance_uuid,
7 has_uuid=has_uuid)
8+ is_datacenter = self.patch(vmware.VMwarePyvmomiAPI, 'is_datacenter')
9+ is_datacenter.side_effect = lambda x: isinstance(
10+ x, FakeVmomiDatacenter)
11+ is_datacenter = self.patch(vmware.VMwarePyvmomiAPI, 'is_vm')
12+ is_datacenter.side_effect = lambda x: isinstance(
13+ x, FakeVmomiVM)
14+ is_folder = self.patch(vmware.VMwarePyvmomiAPI, 'is_folder')
15+ is_folder.side_effect = lambda x: isinstance(
16+ x, (FakeVmomiVmFolder, FakeVmomiRootFolder))
17+ has_children = self.patch(vmware.VMwarePyvmomiAPI, 'has_children')
18+ has_children.side_effect = lambda x: isinstance(
19+ x, (FakeVmomiVmFolder, FakeVmomiDatacenter))
20 return mock_vmomi_api
21
22 def setUp(self):
23@@ -257,7 +269,6 @@
24
25 def test_get_vmware_servers(self):
26 self.configure_vmomi_api(servers=10)
27-
28 servers = vmware.get_vmware_servers(
29 factory.make_hostname(),
30 factory.make_username(),
31
32=== modified file 'src/provisioningserver/drivers/hardware/vmware.py'
33--- src/provisioningserver/drivers/hardware/vmware.py 2016-05-12 19:07:37 +0000
34+++ src/provisioningserver/drivers/hardware/vmware.py 2016-06-30 17:59:16 +0000
35@@ -7,9 +7,14 @@
36 'probe_vmware_and_enlist',
37 ]
38
39-from abc import abstractmethod
40+from abc import (
41+ ABCMeta,
42+ abstractmethod,
43+)
44 from collections import OrderedDict
45 from importlib import import_module
46+from inspect import getcallargs
47+import ssl
48 import traceback
49 from typing import Optional
50 from urllib.parse import unquote
51@@ -64,7 +69,7 @@
52 """The VMware API endpoint could not be contacted."""
53
54
55-class VMwareAPI(object):
56+class VMwareAPI(metaclass=ABCMeta):
57 """Abstract base class to represent a MAAS-capable VMware API. The API
58 must be capable of:
59 - Gathering names, UUID, and MAC addresses of each virtual machine
60@@ -101,11 +106,36 @@
61 """Returns True if the VMware API is thought to be connected"""
62 raise NotImplementedError
63
64+ @abstractmethod
65 def disconnect(self):
66 """Disconnects from the VMware API"""
67 raise NotImplementedError
68
69 @abstractmethod
70+ def is_folder(self, obj):
71+ """Returns true if the specified API object is a Folder."""
72+ raise NotImplementedError
73+
74+ @abstractmethod
75+ def is_datacenter(self, obj):
76+ """Returns true if the specified API object is a Datacenter."""
77+ raise NotImplementedError
78+
79+ @abstractmethod
80+ def is_vm(self, obj):
81+ """Returns true if the specified API object is a VirtualMachine."""
82+ raise NotImplementedError
83+
84+ @abstractmethod
85+ def has_children(self, obj):
86+ """Returns true if the specified API object has children.
87+
88+ This is used to determine if it should be traversed in order to look
89+ for more virutal machines.
90+ """
91+ raise NotImplementedError
92+
93+ @abstractmethod
94 def find_vm_by_uuid(self, uuid):
95 """
96 Searches for a VM that matches the specified UUID. The UUID can be
97@@ -115,9 +145,8 @@
98 """
99 raise NotImplementedError
100
101- @staticmethod
102 @abstractmethod
103- def get_maas_power_state(vm):
104+ def get_maas_power_state(self, vm):
105 """
106 Returns the MAAS representation of the power status for the
107 specified virtual machine.
108@@ -125,9 +154,8 @@
109 """
110 raise NotImplementedError
111
112- @staticmethod
113 @abstractmethod
114- def set_power_state(vm, power_change):
115+ def set_power_state(self, vm, power_change):
116 """
117 Sets the power state for the specified VM to the specified value.
118 :param:power_change: the new desired state ('on' or 'off')
119@@ -158,26 +186,52 @@
120 self.service_instance = None
121
122 def connect(self):
123- # place optional arguments in a dictionary to pass to the
124+ # Place optional arguments in a dictionary to pass to the
125 # VMware API call; otherwise the API will see 'None' and fail.
126 extra_args = {}
127 if self.port is not None:
128 extra_args['port'] = self.port
129-
130 if self.protocol is not None:
131- extra_args['protocol'] = self.protocol
132-
133+ if self.protocol == "https+unverified":
134+ # This is a workaround for using untrusted certificates.
135+ extra_args['protocol'] = 'https'
136+ if 'sslContext' in getcallargs(vmomi_api.SmartConnect):
137+ context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
138+ context.verify_mode = ssl.CERT_NONE
139+ extra_args['sslContext'] = context
140+ else:
141+ maaslog.error(
142+ "Unable to use unverified SSL context to connect to "
143+ "'%s'. (In order to use this feature, you must update "
144+ "to a more recent version of the python3-pyvmomi "
145+ "package.)" % self.host)
146+ raise VMwareAPIException(
147+ "Failed to set up unverified SSL context. Please "
148+ "update to a more recent version of the "
149+ "python3-pyvmomi package.")
150+ else:
151+ extra_args['protocol'] = self.protocol
152 self.service_instance = vmomi_api.SmartConnect(host=self.host,
153 user=self.username,
154 pwd=self.password,
155 **extra_args)
156-
157 if not self.service_instance:
158 raise VMwareAPIConnectionFailed(
159 "Could not connect to VMware service API")
160-
161 return self.service_instance is not None
162
163+ def is_folder(self, obj):
164+ return isinstance(obj, vim.Folder)
165+
166+ def is_datacenter(self, obj):
167+ return isinstance(obj, vim.Datacenter)
168+
169+ def is_vm(self, obj):
170+ return isinstance(obj, vim.VirtualMachine)
171+
172+ def has_children(self, obj):
173+ return isinstance(obj, (vim.Folder, vim.Datacenter))
174+
175 def is_connected(self):
176 return self.service_instance is not None
177
178@@ -185,8 +239,7 @@
179 vmomi_api.Disconnect(self.service_instance)
180 self.service_instance = None
181
182- @staticmethod
183- def _probe_network_cards(vm):
184+ def _probe_network_cards(self, vm):
185 """Returns a list of MAC addresses for this VM, followed by a list
186 of unique keys that VMware uses to uniquely identify the NICs. The
187 MAC addresses are used to create the node. If the node is created
188@@ -202,8 +255,7 @@
189 nic_keys.append(device.key)
190 return mac_addresses, nic_keys
191
192- @staticmethod
193- def _get_uuid(vm):
194+ def _get_uuid(self, vm):
195 # In vCenter environments, using the BIOS UUID (uuid) is deprecated.
196 # But we can use it as a fallback, since the API supports both.
197 if hasattr(vm.summary.config, 'instanceUuid') \
198@@ -214,17 +266,28 @@
199 return vm.summary.config.uuid
200 return None
201
202- def _get_vm_list(self):
203+ def _find_virtual_machines(self, parent):
204 vms = []
205- content = self.service_instance.RetrieveContent()
206- for child in content.rootFolder.childEntity:
207- if hasattr(child, 'vmFolder'):
208- datacenter = child
209- vm_folder = datacenter.vmFolder
210- vm_list = vm_folder.childEntity
211- vms = vms + vm_list
212+ if self.is_datacenter(parent):
213+ children = parent.vmFolder.childEntity
214+ elif self.is_folder(parent):
215+ children = parent.childEntity
216+ else:
217+ raise ValueError("Unknown type: %s" % type(parent))
218+ for child in children:
219+ if self.is_vm(child):
220+ vms.append(child)
221+ elif self.has_children(child):
222+ vms.extend(self._find_virtual_machines(child))
223+ else:
224+ print("Unknown type: %s" % type(child))
225 return vms
226
227+ def _get_vm_list(self):
228+ content = self.service_instance.RetrieveContent()
229+ root_folder = content.rootFolder
230+ return self._find_virtual_machines(root_folder)
231+
232 def find_vm_by_name(self, vm_name):
233 vm_list = self._get_vm_list()
234 for vm in vm_list:
235@@ -243,12 +306,10 @@
236 vm = content.searchIndex.FindByUuid(None, uuid, True, False)
237 return vm
238
239- @staticmethod
240- def _get_power_state(vm):
241+ def _get_power_state(self, vm):
242 return vm.runtime.powerState
243
244- @staticmethod
245- def pyvmomi_to_maas_powerstate(power_state):
246+ def pyvmomi_to_maas_powerstate(self, power_state):
247 """Returns a MAAS power state given the specified pyvmomi state"""
248 if power_state == 'poweredOn':
249 return "on"
250@@ -259,13 +320,10 @@
251 else:
252 return "error"
253
254- @staticmethod
255- def get_maas_power_state(vm):
256- return VMwarePyvmomiAPI.pyvmomi_to_maas_powerstate(
257- vm.runtime.powerState)
258+ def get_maas_power_state(self, vm):
259+ return self.pyvmomi_to_maas_powerstate(vm.runtime.powerState)
260
261- @staticmethod
262- def set_power_state(vm, power_change):
263+ def set_power_state(self, vm, power_change):
264 if vm is not None:
265 if power_change == 'on':
266 vm.PowerOn()
267@@ -276,8 +334,7 @@
268 "set_power_state: Invalid power_change state: {state}"
269 .format(power_change))
270
271- @staticmethod
272- def set_pxe_boot(vm_properties):
273+ def set_pxe_boot(self, vm_properties):
274 boot_devices = []
275 for nic in vm_properties['nics']:
276 boot_nic = vim.vm.BootOptions.BootableEthernetDevice()