Monitor when the windows VM is up and running
[snf-image-creator] / image_creator / os_type / windows.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright 2012 GRNET S.A. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8 #
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12 #
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 #
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 """This module hosts OS-specific code common for the various Microsoft
37 Windows OSs."""
38
39 from image_creator.os_type import OSBase, sysprep
40 from image_creator.util import FatalError, check_guestfs_version, get_command
41
42 import hivex
43 import tempfile
44 import os
45 import time
46 import random
47 import string
48 import subprocess
49
50 kvm = get_command('kvm')
51
52 BOOT_TIMEOUT = 300
53
54
55 class Windows(OSBase):
56     """OS class for Windows"""
57
58     def needed_sysprep_params(self):
59         """Returns a list of needed sysprep parameters. Each element in the
60         list is a SysprepParam object.
61         """
62
63         password = self.SysprepParam(
64             'password', 'Image Administrator Password', 20, lambda x: True)
65
66         return [password]
67
68     @sysprep(enabled=True)
69     def disable_ipv6_privacy_extensions(self, print_header=True):
70         """Disable IPv6 privacy extensions"""
71
72         if print_header:
73             self.out.output("Disabling IPv6 privacy extensions")
74
75         self._guest_exec('netsh interface ipv6 set global '
76                          'randomizeidentifiers=disabled store=persistent')
77
78     @sysprep(enabled=True)
79     def microsoft_sysprep(self, print_header=True):
80         """Run the Microsoft System Preparation Tool on the Image. This will
81         remove system-specific data and will make the image ready to be
82         deployed. After this no other task may run.
83         """
84
85         if print_header:
86             self.out.output("Executing sysprep on the image (may take more "
87                             "than 10 minutes)")
88
89         self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
90                          r'/quiet /generalize /oobe /shutdown')
91         self.syspreped = True
92
93     def do_sysprep(self):
94         """Prepare system for image creation."""
95
96         if getattr(self, 'syspreped', False):
97             raise FatalError("Image is already syspreped!")
98
99         txt = "System preparation parameter: `%s' is needed but missing!"
100         for param in self.needed_sysprep_params():
101             if param[0] not in self.sysprep_params:
102                 raise FatalError(txt % param[0])
103
104         self.mount(readonly=False)
105         try:
106             disabled_uac = self._update_uac_remote_setting(1)
107             token = self._enable_os_monitor()
108         finally:
109             self.umount()
110
111         self.out.output("Shutting down helper VM ...", False)
112         self.g.sync()
113         # guestfs_shutdown which is the prefered way to shutdown the backend
114         # process was introduced in version 1.19.16
115         if check_guestfs_version(self.g, 1, 19, 16) >= 0:
116             ret = self.g.shutdown()
117         else:
118             ret = self.g.kill_subprocess()
119
120         self.out.success('done')
121
122         vm = None
123         monitor = None
124         try:
125             self.out.output("Starting windows VM ...", False)
126             monitorfd, monitor = tempfile.mkstemp()
127             os.close(monitorfd)
128             vm, display = self._create_vm(monitor)
129             self.out.success("started (console on vnc display: %d)." % display)
130
131             self.out.output("Waiting for OS to boot ...", False)
132             if not self._wait_on_file(monitor, token):
133                 raise FatalError("Windows booting timed out.")
134             else:
135                 self.out.success('done')
136
137             self.out.output("Disabling automatic logon ...", False)
138             self._disable_autologon()
139             self.out.success('done')
140
141             self.out.output('Preparing system from image creation:')
142
143             tasks = self.list_syspreps()
144             enabled = filter(lambda x: x.enabled, tasks)
145
146             size = len(enabled)
147
148             # Make sure the ms sysprep is the last task to run if it is enabled
149             enabled = filter(
150                 lambda x: x.im_func.func_name != 'microsoft_sysprep', enabled)
151
152             ms_sysprep_enabled = False
153             if len(enabled) != size:
154                 enabled.append(self.ms_sysprep)
155                 ms_sysprep_enabled = True
156
157             cnt = 0
158             for task in enabled:
159                 cnt += 1
160                 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
161                 task()
162                 setattr(task.im_func, 'executed', True)
163
164             self.out.output("Shutting down windows VM ...", False)
165             if not ms_sysprep_enabled:
166                 self._shutdown()
167             self.out.success("done")
168
169             vm.wait()
170         finally:
171             if monitor is not None:
172                 os.unlink(monitor)
173
174             if vm is not None:
175                 self._destroy_vm(vm)
176
177             self.out.output("Relaunching helper VM (may take a while) ...",
178                             False)
179             self.g.launch()
180             self.out.success('done')
181
182         if disabled_uac:
183             self._update_uac_remote_setting(0)
184
185     def _create_vm(self, monitor):
186         """Create a VM with the image attached as the disk
187
188             monitor: a file to be used to monitor when the OS is up
189         """
190
191         def random_mac():
192             mac = [0x00, 0x16, 0x3e,
193                    random.randint(0x00, 0x7f),
194                    random.randint(0x00, 0xff),
195                    random.randint(0x00, 0xff)]
196
197             return ':'.join(map(lambda x: "%02x" % x, mac))
198
199         # Use ganeti's VNC port range for a random vnc port
200         vnc_port = random.randint(11000, 14999)
201         display = vnc_port - 5900
202
203         vm = kvm('-smp', '1', '-m', '1024', '-drive',
204                  'file=%s,format=raw,cache=none,if=virtio' % self.image.device,
205                  '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
206                  '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' %
207                  random_mac(), '-vnc', ':%d' % display, '-serial',
208                  'file:%s' % monitor, _bg=True)
209
210         return vm, display
211
212     def _destroy_vm(self, vm):
213         """Destroy a VM previously created by _create_vm"""
214         if vm.process.alive:
215             vm.terminate()
216
217     def _shutdown(self):
218         """Shuts down the windows VM"""
219         self._guest_exec(r'shutdown /s /t 5')
220
221     def _wait_on_file(self, fname, msg):
222         """Wait until a message appears on a file"""
223
224         for i in range(BOOT_TIMEOUT):
225             time.sleep(1)
226             with open(fname) as f:
227                 for line in f:
228                     if line.startswith(msg):
229                         return True
230         return False
231
232     def _disable_autologon(self):
233         """Disable automatic logon on the windows image"""
234
235         winlogon = \
236             r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
237
238         self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
239         self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
240         self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
241
242     def _registry_file_path(self, regfile):
243         """Retrieves the case sensitive path to a registry file"""
244
245         systemroot = self.g.inspect_get_windows_systemroot(self.root)
246         path = "%s/system32/config/%s" % (systemroot, regfile)
247         try:
248             path = self.g.case_sensitive_path(path)
249         except RuntimeError as e:
250             raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
251                              % (regfile, str(e)))
252         return path
253
254     def _enable_os_monitor(self):
255         """Add a script in the registry that will send a random string to the
256         first serial port when the windows image finishes booting.
257         """
258
259         token = "".join(random.choice(string.ascii_letters) for x in range(16))
260
261         path = self._registry_file_path('SOFTWARE')
262         softwarefd, software = tempfile.mkstemp()
263         try:
264             os.close(softwarefd)
265             self.g.download(path, software)
266
267             h = hivex.Hivex(software, write=True)
268
269             # Enable automatic logon.
270             # This is needed because we need to execute a script that we add in
271             # the RunOnce registry entry and those programs only get executed
272             # when a user logs on. There is a RunServicesOnce registry entry
273             # whose keys get executed in the background when the logon dialog
274             # box first appears, but they seem to only work with services and
275             # not arbitrary command line expressions :-(
276             #
277             # Instructions on how to turn on automatic logon in Windows can be
278             # found here: http://support.microsoft.com/kb/324737
279             #
280             # Warning: Registry change will not work if the “Logon Banner” is
281             # defined on the server either by a Group Policy object (GPO) or by
282             # a local policy.
283
284             winlogon = h.root()
285             for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
286                           'Winlogon'):
287                 winlogon = h.node_get_child(winlogon, child)
288
289             h.node_set_value(
290                 winlogon,
291                 {'key': 'DefaultUserName', 't': 1,
292                  'value': "Administrator".encode('utf-16le')})
293             h.node_set_value(
294                 winlogon,
295                 {'key': 'DefaultPassword', 't': 1,
296                  'value':  self.sysprep_params['password'].encode('utf-16le')})
297             h.node_set_value(
298                 winlogon,
299                 {'key': 'AutoAdminLogon', 't': 1,
300                  'value': "1".encode('utf-16le')})
301
302             key = h.root()
303             for child in ('Microsoft', 'Windows', 'CurrentVersion'):
304                 key = h.node_get_child(key, child)
305
306             runonce = h.node_get_child(key, "RunOnce")
307             if runonce is None:
308                 runonce = h.node_add_child(key, "RunOnce")
309
310             value = (
311                 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
312                 r'-ExecutionPolicy RemoteSigned '
313                 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
314                 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
315                 r'$port.Close()}"').encode('utf-16le')
316
317             h.node_set_value(runonce,
318                              {'key': "BootMonitor", 't': 1, 'value': value})
319
320             h.commit(None)
321
322             self.g.upload(software, path)
323         finally:
324             os.unlink(software)
325
326         return token
327
328     def _update_uac_remote_setting(self, value):
329         """Updates the registry key value:
330         [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
331         \System]"LocalAccountTokenFilterPolicy"
332
333         value = 1 will disable the UAC remote restrictions
334         value = 0 will enable the UAC remote restrictions
335
336         For more info see here: http://support.microsoft.com/kb/951016
337
338         Returns:
339             True if the key is changed
340             False if the key is unchanged
341         """
342
343         if value not in (0, 1):
344             raise ValueError("Valid values for value parameter are 0 and 1")
345
346         path = self._registry_file_path('SOFTWARE')
347         softwarefd, software = tempfile.mkstemp()
348         try:
349             os.close(softwarefd)
350             self.g.download(path, software)
351
352             h = hivex.Hivex(software, write=True)
353
354             key = h.root()
355             for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
356                           'System'):
357                 key = h.node_get_child(key, child)
358
359             policy = None
360             for val in h.node_values(key):
361                 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
362                     policy = val
363
364             if policy is not None:
365                 dword = h.value_dword(policy)
366                 if dword == value:
367                     return False
368             elif value == 0:
369                 return False
370
371             new_value = {
372                 'key': "LocalAccountTokenFilterPolicy", 't': 4L,
373                 'value': '%s\x00\x00\x00' % '\x00' if value == 0 else '\x01'}
374
375             h.node_set_value(key, new_value)
376             h.commit(None)
377
378             self.g.upload(software, path)
379
380         finally:
381             os.unlink(software)
382
383         return True
384
385     def _do_collect_metadata(self):
386         """Collect metadata about the OS"""
387         super(Windows, self)._do_collect_metadata()
388         self.meta["USERS"] = " ".join(self._get_users())
389
390     def _get_users(self):
391         """Returns a list of users found in the images"""
392         path = self._registry_file_path('SAM')
393         samfd, sam = tempfile.mkstemp()
394         try:
395             os.close(samfd)
396             self.g.download(path, sam)
397
398             h = hivex.Hivex(sam)
399
400             key = h.root()
401             # Navigate to /SAM/Domains/Account/Users/Names
402             for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
403                 key = h.node_get_child(key, child)
404
405             users = [h.node_name(x) for x in h.node_children(key)]
406
407         finally:
408             os.unlink(sam)
409
410         # Filter out the guest account
411         return filter(lambda x: x != "Guest", users)
412
413     def _guest_exec(self, command, fatal=True):
414         """Execute a command on a windows VM"""
415
416         user = "Administrator%" + self.sysprep_params['password']
417         addr = 'localhost'
418         runas = '--runas=%s' % user
419         winexe = subprocess.Popen(
420             ['winexe', '-U', user, "//%s" % addr, runas, command],
421             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
422
423         stdout, stderr = winexe.communicate()
424         rc = winexe.poll()
425
426         if rc != 0 and fatal:
427             reason = stderr if len(stderr) else stdout
428             raise FatalError("Command: `%s' failed. Reason: %s" %
429                              (command, reason))
430
431         return (stdout, stderr, rc)
432
433 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :