1 # -*- coding: utf-8 -*-
3 # Copyright 2012 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
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.
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.
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.
36 """This module hosts OS-specific code common for the various Microsoft
39 from image_creator.os_type import OSBase, sysprep
40 from image_creator.util import FatalError, check_guestfs_version, get_command
51 kvm = get_command('kvm')
55 # For more info see: http://technet.microsoft.com/en-us/library/jj612867.aspx
56 KMS_CLIENT_SETUP_KEYS = {
57 "Windows 8 Professional": "NG4HW-VH26C-733KW-K6F98-J8CK4",
58 "Windows 8 Professional N": "XCVCF-2NXM9-723PB-MHCB7-2RYQQ",
59 "Windows 8 Enterprise": "32JNW-9KQ84-P47T8-D8GGY-CWCK7",
60 "Windows 8 Enterprise N": "JMNMF-RHW7P-DMY6X-RF3DR-X2BQT",
61 "Windows Server 2012 Core": "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
62 "Windows Server 2012 Core N": "8N2M2-HWPGY-7PGT9-HGDD8-GVGGY",
63 "Windows Server 2012 Core Single Language":
64 "2WN2H-YGCQR-KFX6K-CD6TF-84YXQ",
65 "Windows Server 2012 Core Country Specific":
66 "4K36P-JN4VD-GDC6V-KDT89-DYFKP",
67 "Windows Server 2012 Server Standard": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
68 "Windows Server 2012 Standard Core": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
69 "Windows Server 2012 MultiPoint Standard": "HM7DN-YVMH3-46JC3-XYTG7-CYQJJ",
70 "Windows Server 2012 MultiPoint Premium": "XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G",
71 "Windows Server 2012 Datacenter": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
72 "Windows Server 2012 Datacenter Core": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
73 "Windows 7 Professional": "FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4",
74 "Windows 7 Professional N": "MRPKT-YTG23-K7D7T-X2JMM-QY7MG",
75 "Windows 7 Professional E": "W82YF-2Q76Y-63HXB-FGJG9-GF7QX",
76 "Windows 7 Enterprise": "33PXH-7Y6KF-2VJC9-XBBR8-HVTHH",
77 "Windows 7 Enterprise N": "YDRBP-3D83W-TY26F-D46B2-XCKRJ",
78 "Windows 7 Enterprise E": "C29WB-22CC8-VJ326-GHFJW-H9DH4",
79 "Windows Server 2008 R2 Web": "6TPJF-RBVHG-WBW2R-86QPH-6RTM4",
80 "Windows Server 2008 R2 HPC edition": "TT8MH-CG224-D3D7Q-498W2-9QCTX",
81 "Windows Server 2008 R2 Standard": "YC6KT-GKW9T-YTKYR-T4X34-R7VHC",
82 "Windows Server 2008 R2 Enterprise": "489J6-VHDMP-X63PK-3K798-CPX3Y",
83 "Windows Server 2008 R2 Datacenter": "74YFP-3QFB3-KQT8W-PMXWJ-7M648",
84 "Windows Server 2008 R2 for Itanium-based Systems":
85 "GT63C-RJFQ3-4GMB6-BRFB9-CB83V",
86 "Windows Vista Business": "YFKBB-PQJJV-G996G-VWGXY-2V3X8",
87 "Windows Vista Business N": "HMBQG-8H2RH-C77VX-27R82-VMQBT",
88 "Windows Vista Enterprise": "VKK3X-68KWM-X2YGT-QR4M6-4BWMV",
89 "Windows Vista Enterprise N": "VTC42-BM838-43QHV-84HX6-XJXKV",
90 "Windows Web Server 2008": "WYR28-R7TFJ-3X2YQ-YCY4H-M249D",
91 "Windows Server 2008 Standard": "TM24T-X9RMF-VWXK6-X8JC9-BFGM2",
92 "Windows Server 2008 Standard without Hyper-V":
93 "W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ",
94 "Windows Server 2008 Enterprise":
95 "YQGMW-MPWTJ-34KDK-48M3W-X4Q6V",
96 "Windows Server 2008 Enterprise without Hyper-V":
97 "39BXF-X8Q23-P2WWT-38T2F-G3FPG",
98 "Windows Server 2008 HPC": "RCTX3-KWVHP-BR6TB-RB6DM-6X7HP",
99 "Windows Server 2008 Datacenter": "7M67G-PC374-GR742-YH8V4-TCBY3",
100 "Windows Server 2008 Datacenter without Hyper-V":
101 "22XQ2-VRXRG-P8D42-K34TD-G3QQC",
102 "Windows Server 2008 for Itanium-Based Systems":
103 "4DWFP-JF3DJ-B7DTH-78FJB-PDRHK"}
106 class Windows(OSBase):
107 """OS class for Windows"""
108 def __init__(self, image, **kargs):
109 super(Windows, self).__init__(image, **kargs)
111 device = self.g.part_to_dev(self.root)
113 self.last_part_num = self.g.part_list(device)[-1]['part_num']
114 self.last_drive = None
115 self.system_drive = None
117 for drive, partition in self.g.inspect_get_drive_mappings(self.root):
118 if partition == "%s%d" % (device, self.last_part_num):
119 self.last_drive = drive
120 if partition == self.root:
121 self.system_drive = drive
123 assert self.system_drive
125 self.product_name = self.g.inspect_get_product_name(self.root)
127 def needed_sysprep_params(self):
128 """Returns a list of needed sysprep parameters. Each element in the
129 list is a SysprepParam object.
131 password = self.SysprepParam(
132 'password', 'Image Administrator Password', 20, lambda x: True)
136 @sysprep('Disabling IPv6 privacy extensions')
137 def disable_ipv6_privacy_extensions(self):
138 """Disable IPv6 privacy extensions"""
140 self._guest_exec('netsh interface ipv6 set global '
141 'randomizeidentifiers=disabled store=persistent')
143 @sysprep('Disabling Teredo interface')
144 def disable_teredo(self):
145 """Disable Teredo interface"""
147 self._guest_exec('netsh interface teredo set state disabled')
149 @sysprep('Disabling ISATAP Adapters')
150 def disable_isatap(self):
151 """Disable ISATAP Adapters"""
153 self._guest_exec('netsh interface isa set state disabled')
155 @sysprep('Enabling ping responses')
156 def enable_pings(self):
157 """Enable ping responces"""
159 self._guest_exec('netsh firewall set icmpsetting 8')
161 @sysprep('Disabling hibernation support')
162 def disable_hibernation(self):
163 """Disable hibernation support and remove the hibernation file"""
165 self._guest_exec(r'powercfg.exe /hibernate off')
167 @sysprep('Setting the system clock to UTC')
169 """Set the hardware clock to UTC"""
171 path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
173 r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
175 @sysprep('Clearing the event logs')
176 def clear_logs(self):
177 """Clear all the event logs"""
180 r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
182 @sysprep('Executing sysprep on the image (may take more that 10 minutes)')
183 def microsoft_sysprep(self):
184 """Run the Microsoft System Preparation Tool. This will remove
185 system-specific data and will make the image ready to be deployed.
186 After this no other task may run.
189 self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
190 r'/quiet /generalize /oobe /shutdown')
191 self.syspreped = True
193 @sysprep('Converting the image into a KMS client', enabled=False)
194 def kms_client_setup(self):
195 """Install the appropriate KMS client setup key to the image to convert
196 it to a KMS client. Computers that are running volume licensing
197 editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
198 2008 R2, Windows Vista, and Windows Server 2008 are, by default, KMS
199 clients with no additional configuration needed.
202 setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
205 "Don't know the KMS client setup key for product: `%s'" %
210 "cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
212 @sysprep('Shrinking the last filesystem')
214 """Shrink the last filesystem. Make sure the filesystem is defragged"""
216 # Query for the maximum number of reclaimable bytes
218 r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
219 r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
220 'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
221 r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
222 r'ECHO EXIT >> %SCRIPT% & ' +
223 r'DISKPART /S %SCRIPT% & ' +
224 r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
227 stdout, stderr, rc = self._guest_exec(cmd)
230 for line in stdout.splitlines():
231 # diskpart will return something like this:
233 # The maximum number of reclaimable bytes is: xxxx MB
235 if line.find('reclaimable') >= 0:
236 querymax = line.split(':')[1].split()[0].strip()
237 assert querymax.isdigit(), \
238 "Number of reclaimable bytes not a number"
241 FatalError("Error in shrinking! "
242 "Couldn't find the max number of reclaimable bytes!")
244 querymax = int(querymax)
246 # Practically the smallest shrunken size generally is at around
247 # "used space" + (20-200 MB). Please also take into account that
248 # Windows might need about 50-100 MB free space left to boot safely.
249 # I'll give 100MB extra space just to be sure
253 self.out.warn("Not enought available space to shrink the image!")
257 r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
258 r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
259 'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
260 'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
261 r'ECHO EXIT >> %SCRIPT% & ' +
262 r'DISKPART /S %SCRIPT% & ' +
263 r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
266 stdout, stderr, rc = self._guest_exec(cmd)
268 for line in stdout.splitlines():
269 if line.find('shrunk') >= 0:
270 self.out.output(line)
272 def do_sysprep(self):
273 """Prepare system for image creation."""
275 if getattr(self, 'syspreped', False):
276 raise FatalError("Image is already syspreped!")
278 txt = "System preparation parameter: `%s' is needed but missing!"
279 for param in self.needed_sysprep_params():
280 if param[0] not in self.sysprep_params:
281 raise FatalError(txt % param[0])
283 self.mount(readonly=False)
285 disabled_uac = self._update_uac_remote_setting(1)
286 token = self._enable_os_monitor()
288 # disable the firewalls
289 firewall_states = self._update_firewalls(0, 0, 0)
291 # Delete the pagefile. It will be recreated when the system boots
292 systemroot = self.g.inspect_get_windows_systemroot(self.root)
293 pagefile = "%s/pagefile.sys" % systemroot
294 self.g.rm_rf(self.g.case_sensitive_path(pagefile))
299 self.out.output("Shutting down helper VM ...", False)
301 # guestfs_shutdown which is the prefered way to shutdown the backend
302 # process was introduced in version 1.19.16
303 if check_guestfs_version(self.g, 1, 19, 16) >= 0:
304 ret = self.g.shutdown()
306 ret = self.g.kill_subprocess()
308 self.out.success('done')
313 self.out.output("Starting windows VM ...", False)
314 monitorfd, monitor = tempfile.mkstemp()
316 vm, display = self._create_vm(monitor)
317 self.out.success("started (console on vnc display: %d)." % display)
319 self.out.output("Waiting for OS to boot ...", False)
320 if not self._wait_on_file(monitor, token):
321 raise FatalError("Windows booting timed out.")
323 time.sleep(10) # Just to be sure everything is up
324 self.out.success('done')
326 self.out.output("Disabling automatic logon ...", False)
327 self._disable_autologon()
328 self.out.success('done')
330 self.out.output('Preparing system from image creation:')
332 tasks = self.list_syspreps()
333 enabled = filter(lambda x: x.enabled, tasks)
336 # Make sure shrink runs in the end, before ms sysprep
337 enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
340 shrink_enabled = False
341 if len(enabled) != size:
342 enabled.append(self.shrink)
343 shrink_enabled = True
345 # Make sure the ms sysprep is the last task to run if it is enabled
347 lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
350 ms_sysprep_enabled = False
351 if len(enabled) != size:
352 enabled.append(self.microsoft_sysprep)
353 ms_sysprep_enabled = True
358 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
360 setattr(task.im_func, 'executed', True)
362 self.out.output("Sending shut down command ...", False)
363 if not ms_sysprep_enabled:
365 self.out.success("done")
367 self.out.output("Waiting for windows to shut down ...", False)
369 self.out.success("done")
371 if monitor is not None:
377 self.out.output("Relaunching helper VM (may take a while) ...",
380 self.out.success('done')
382 self.mount(readonly=False)
385 self._update_uac_remote_setting(0)
387 self._update_firewalls(*firewall_states)
391 def _create_vm(self, monitor):
392 """Create a VM with the image attached as the disk
394 monitor: a file to be used to monitor when the OS is up
398 mac = [0x00, 0x16, 0x3e,
399 random.randint(0x00, 0x7f),
400 random.randint(0x00, 0xff),
401 random.randint(0x00, 0xff)]
403 return ':'.join(map(lambda x: "%02x" % x, mac))
405 # Use ganeti's VNC port range for a random vnc port
406 vnc_port = random.randint(11000, 14999)
407 display = vnc_port - 5900
410 '-smp', '1', '-m', '1024', '-drive',
411 'file=%s,format=raw,cache=unsafe,if=virtio' % self.image.device,
412 '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
413 '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
414 '-vnc', ':%d' % display, '-serial', 'file:%s' % monitor, _bg=True)
418 def _destroy_vm(self, vm):
419 """Destroy a VM previously created by _create_vm"""
424 """Shuts down the windows VM"""
425 self._guest_exec(r'shutdown /s /t 5')
427 def _wait_on_file(self, fname, msg):
428 """Wait until a message appears on a file"""
430 for i in range(BOOT_TIMEOUT):
432 with open(fname) as f:
434 if line.startswith(msg):
438 def _disable_autologon(self):
439 """Disable automatic logon on the windows image"""
442 r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
444 self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
445 self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
446 self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
448 def _registry_file_path(self, regfile):
449 """Retrieves the case sensitive path to a registry file"""
451 systemroot = self.g.inspect_get_windows_systemroot(self.root)
452 path = "%s/system32/config/%s" % (systemroot, regfile)
454 path = self.g.case_sensitive_path(path)
455 except RuntimeError as e:
456 raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
460 def _enable_os_monitor(self):
461 """Add a script in the registry that will send a random string to the
462 first serial port when the windows image finishes booting.
465 token = "".join(random.choice(string.ascii_letters) for x in range(16))
467 path = self._registry_file_path('SOFTWARE')
468 softwarefd, software = tempfile.mkstemp()
471 self.g.download(path, software)
473 h = hivex.Hivex(software, write=True)
475 # Enable automatic logon.
476 # This is needed because we need to execute a script that we add in
477 # the RunOnce registry entry and those programs only get executed
478 # when a user logs on. There is a RunServicesOnce registry entry
479 # whose keys get executed in the background when the logon dialog
480 # box first appears, but they seem to only work with services and
481 # not arbitrary command line expressions :-(
483 # Instructions on how to turn on automatic logon in Windows can be
484 # found here: http://support.microsoft.com/kb/324737
486 # Warning: Registry change will not work if the “Logon Banner” is
487 # defined on the server either by a Group Policy object (GPO) or by
491 for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
493 winlogon = h.node_get_child(winlogon, child)
497 {'key': 'DefaultUserName', 't': 1,
498 'value': "Administrator".encode('utf-16le')})
501 {'key': 'DefaultPassword', 't': 1,
502 'value': self.sysprep_params['password'].encode('utf-16le')})
505 {'key': 'AutoAdminLogon', 't': 1,
506 'value': "1".encode('utf-16le')})
509 for child in ('Microsoft', 'Windows', 'CurrentVersion'):
510 key = h.node_get_child(key, child)
512 runonce = h.node_get_child(key, "RunOnce")
514 runonce = h.node_add_child(key, "RunOnce")
517 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
518 r'-ExecutionPolicy RemoteSigned '
519 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
520 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
521 r'$port.Close()}"').encode('utf-16le')
523 h.node_set_value(runonce,
524 {'key': "BootMonitor", 't': 1, 'value': value})
527 r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
528 r'\policies\system /v LocalAccountTokenFilterPolicy'
529 r' /t REG_DWORD /d 1 /f').encode('utf-16le')
531 h.node_set_value(runonce,
532 {'key': "UpdateRegistry", 't': 1, 'value': value})
536 self.g.upload(software, path)
542 def _update_firewalls(self, domain, public, standard):
543 """Enables or disables the firewall for the Domain, the Public and the
544 Standard profile. Returns a triplete with the old values.
546 1 will enable a firewall and 0 will disable it
549 if domain not in (0, 1):
550 raise ValueError("Valid values for domain parameter are 0 and 1")
552 if public not in (0, 1):
553 raise ValueError("Valid values for public parameter are 0 and 1")
555 if standard not in (0, 1):
556 raise ValueError("Valid values for standard parameter are 0 and 1")
558 path = self._registry_file_path("SYSTEM")
559 systemfd, system = tempfile.mkstemp()
562 self.g.download(path, system)
564 h = hivex.Hivex(system, write=True)
566 select = h.node_get_child(h.root(), 'Select')
567 current_value = h.node_get_value(select, 'Current')
569 # expecting a little endian dword
570 assert h.value_type(current_value)[1] == 4
571 current = "%03d" % h.value_dword(current_value)
573 firewall_policy = h.root()
574 for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
575 'Parameters', 'FirewallPolicy'):
576 firewall_policy = h.node_get_child(firewall_policy, child)
579 new_values = [domain, public, standard]
580 for profile in ('Domain', 'Public', 'Standard'):
581 node = h.node_get_child(firewall_policy, '%sProfile' % profile)
583 old_value = h.node_get_value(node, 'EnableFirewall')
585 # expecting a little endian dword
586 assert h.value_type(old_value)[1] == 4
587 old_values.append(h.value_dword(old_value))
590 node, {'key': 'EnableFirewall', 't': 4L,
591 'value': struct.pack("<I", new_values.pop(0))})
594 self.g.upload(system, path)
601 def _update_uac_remote_setting(self, value):
602 """Updates the registry key value:
603 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
604 \System]"LocalAccountTokenFilterPolicy"
606 value = 1 will disable the UAC remote restrictions
607 value = 0 will enable the UAC remote restrictions
609 For more info see here: http://support.microsoft.com/kb/951016
612 True if the key is changed
613 False if the key is unchanged
616 if value not in (0, 1):
617 raise ValueError("Valid values for value parameter are 0 and 1")
619 path = self._registry_file_path('SOFTWARE')
620 softwarefd, software = tempfile.mkstemp()
623 self.g.download(path, software)
625 h = hivex.Hivex(software, write=True)
628 for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
630 key = h.node_get_child(key, child)
633 for val in h.node_values(key):
634 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
637 if policy is not None:
638 dword = h.value_dword(policy)
644 new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
645 'value': struct.pack("<I", value)}
647 h.node_set_value(key, new_value)
650 self.g.upload(software, path)
657 def _do_collect_metadata(self):
658 """Collect metadata about the OS"""
659 super(Windows, self)._do_collect_metadata()
660 self.meta["USERS"] = " ".join(self._get_users())
662 def _get_users(self):
663 """Returns a list of users found in the images"""
664 path = self._registry_file_path('SAM')
665 samfd, sam = tempfile.mkstemp()
668 self.g.download(path, sam)
673 # Navigate to /SAM/Domains/Account/Users/Names
674 for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
675 key = h.node_get_child(key, child)
677 users = [h.node_name(x) for x in h.node_children(key)]
682 # Filter out the guest account
683 return filter(lambda x: x != "Guest", users)
685 def _guest_exec(self, command, fatal=True):
686 """Execute a command on a windows VM"""
688 user = "Administrator%" + self.sysprep_params['password']
690 runas = '--runas=%s' % user
691 winexe = subprocess.Popen(
692 ['winexe', '-U', user, runas, "--uninstall", "//%s" % addr,
693 command], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
695 stdout, stderr = winexe.communicate()
698 if rc != 0 and fatal:
699 reason = stderr if len(stderr) else stdout
700 self.out.output("Command: `%s' failed. Reason: %s" %
702 raise FatalError("Command: `%s' failed. Reason: %s" %
705 return (stdout, stderr, rc)
707 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :