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
41 from image_creator.winexe import WinEXE, WinexeTimeout
52 kvm = get_command('kvm')
56 # For more info see: http://technet.microsoft.com/en-us/library/jj612867.aspx
57 KMS_CLIENT_SETUP_KEYS = {
58 "Windows 8 Professional": "NG4HW-VH26C-733KW-K6F98-J8CK4",
59 "Windows 8 Professional N": "XCVCF-2NXM9-723PB-MHCB7-2RYQQ",
60 "Windows 8 Enterprise": "32JNW-9KQ84-P47T8-D8GGY-CWCK7",
61 "Windows 8 Enterprise N": "JMNMF-RHW7P-DMY6X-RF3DR-X2BQT",
62 "Windows Server 2012 Core": "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
63 "Windows Server 2012 Core N": "8N2M2-HWPGY-7PGT9-HGDD8-GVGGY",
64 "Windows Server 2012 Core Single Language":
65 "2WN2H-YGCQR-KFX6K-CD6TF-84YXQ",
66 "Windows Server 2012 Core Country Specific":
67 "4K36P-JN4VD-GDC6V-KDT89-DYFKP",
68 "Windows Server 2012 Server Standard": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
69 "Windows Server 2012 Standard Core": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
70 "Windows Server 2012 MultiPoint Standard": "HM7DN-YVMH3-46JC3-XYTG7-CYQJJ",
71 "Windows Server 2012 MultiPoint Premium": "XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G",
72 "Windows Server 2012 Datacenter": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
73 "Windows Server 2012 Datacenter Core": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
74 "Windows 7 Professional": "FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4",
75 "Windows 7 Professional N": "MRPKT-YTG23-K7D7T-X2JMM-QY7MG",
76 "Windows 7 Professional E": "W82YF-2Q76Y-63HXB-FGJG9-GF7QX",
77 "Windows 7 Enterprise": "33PXH-7Y6KF-2VJC9-XBBR8-HVTHH",
78 "Windows 7 Enterprise N": "YDRBP-3D83W-TY26F-D46B2-XCKRJ",
79 "Windows 7 Enterprise E": "C29WB-22CC8-VJ326-GHFJW-H9DH4",
80 "Windows Server 2008 R2 Web": "6TPJF-RBVHG-WBW2R-86QPH-6RTM4",
81 "Windows Server 2008 R2 HPC edition": "TT8MH-CG224-D3D7Q-498W2-9QCTX",
82 "Windows Server 2008 R2 Standard": "YC6KT-GKW9T-YTKYR-T4X34-R7VHC",
83 "Windows Server 2008 R2 Enterprise": "489J6-VHDMP-X63PK-3K798-CPX3Y",
84 "Windows Server 2008 R2 Datacenter": "74YFP-3QFB3-KQT8W-PMXWJ-7M648",
85 "Windows Server 2008 R2 for Itanium-based Systems":
86 "GT63C-RJFQ3-4GMB6-BRFB9-CB83V",
87 "Windows Vista Business": "YFKBB-PQJJV-G996G-VWGXY-2V3X8",
88 "Windows Vista Business N": "HMBQG-8H2RH-C77VX-27R82-VMQBT",
89 "Windows Vista Enterprise": "VKK3X-68KWM-X2YGT-QR4M6-4BWMV",
90 "Windows Vista Enterprise N": "VTC42-BM838-43QHV-84HX6-XJXKV",
91 "Windows Web Server 2008": "WYR28-R7TFJ-3X2YQ-YCY4H-M249D",
92 "Windows Server 2008 Standard": "TM24T-X9RMF-VWXK6-X8JC9-BFGM2",
93 "Windows Server 2008 Standard without Hyper-V":
94 "W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ",
95 "Windows Server 2008 Enterprise":
96 "YQGMW-MPWTJ-34KDK-48M3W-X4Q6V",
97 "Windows Server 2008 Enterprise without Hyper-V":
98 "39BXF-X8Q23-P2WWT-38T2F-G3FPG",
99 "Windows Server 2008 HPC": "RCTX3-KWVHP-BR6TB-RB6DM-6X7HP",
100 "Windows Server 2008 Datacenter": "7M67G-PC374-GR742-YH8V4-TCBY3",
101 "Windows Server 2008 Datacenter without Hyper-V":
102 "22XQ2-VRXRG-P8D42-K34TD-G3QQC",
103 "Windows Server 2008 for Itanium-Based Systems":
104 "4DWFP-JF3DJ-B7DTH-78FJB-PDRHK"}
107 class Windows(OSBase):
108 """OS class for Windows"""
109 def __init__(self, image, **kargs):
110 super(Windows, self).__init__(image, **kargs)
112 device = self.g.part_to_dev(self.root)
114 self.last_part_num = self.g.part_list(device)[-1]['part_num']
115 self.last_drive = None
116 self.system_drive = None
118 for drive, partition in self.g.inspect_get_drive_mappings(self.root):
119 if partition == "%s%d" % (device, self.last_part_num):
120 self.last_drive = drive
121 if partition == self.root:
122 self.system_drive = drive
124 assert self.system_drive
126 self.product_name = self.g.inspect_get_product_name(self.root)
128 def needed_sysprep_params(self):
129 """Returns a list of needed sysprep parameters. Each element in the
130 list is a SysprepParam object.
132 password = self.SysprepParam(
133 'password', 'Image Administrator Password', 20, lambda x: True)
137 @sysprep('Disabling IPv6 privacy extensions')
138 def disable_ipv6_privacy_extensions(self):
139 """Disable IPv6 privacy extensions"""
141 self._guest_exec('netsh interface ipv6 set global '
142 'randomizeidentifiers=disabled store=persistent')
144 @sysprep('Disabling Teredo interface')
145 def disable_teredo(self):
146 """Disable Teredo interface"""
148 self._guest_exec('netsh interface teredo set state disabled')
150 @sysprep('Disabling ISATAP Adapters')
151 def disable_isatap(self):
152 """Disable ISATAP Adapters"""
154 self._guest_exec('netsh interface isa set state disabled')
156 @sysprep('Enabling ping responses')
157 def enable_pings(self):
158 """Enable ping responses"""
160 self._guest_exec('netsh firewall set icmpsetting 8')
162 @sysprep('Disabling hibernation support')
163 def disable_hibernation(self):
164 """Disable hibernation support and remove the hibernation file"""
166 self._guest_exec(r'powercfg.exe /hibernate off')
168 @sysprep('Setting the system clock to UTC')
170 """Set the hardware clock to UTC"""
172 path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
174 r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
176 @sysprep('Clearing the event logs')
177 def clear_logs(self):
178 """Clear all the event logs"""
181 r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
183 @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
184 def microsoft_sysprep(self):
185 """Run the Microsoft System Preparation Tool. This will remove
186 system-specific data and will make the image ready to be deployed.
187 After this no other task may run.
190 self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
191 r'/quiet /generalize /oobe /shutdown')
192 self.syspreped = True
194 @sysprep('Converting the image into a KMS client', enabled=False)
195 def kms_client_setup(self):
196 """Install the appropriate KMS client setup key to the image to convert
197 it to a KMS client. Computers that are running volume licensing
198 editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
199 2008 R2, Windows Vista, and Windows Server 2008 are, by default, KMS
200 clients with no additional configuration needed.
203 setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
206 "Don't know the KMS client setup key for product: `%s'" %
211 "cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
213 @sysprep('Shrinking the last filesystem')
215 """Shrink the last filesystem. Make sure the filesystem is defragged"""
217 # Query for the maximum number of reclaimable bytes
219 r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
220 r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
221 'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
222 r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
223 r'ECHO EXIT >> %SCRIPT% & ' +
224 r'DISKPART /S %SCRIPT% & ' +
225 r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
228 stdout, stderr, rc = self._guest_exec(cmd)
231 for line in stdout.splitlines():
232 # diskpart will return something like this:
234 # The maximum number of reclaimable bytes is: xxxx MB
236 if line.find('reclaimable') >= 0:
237 querymax = line.split(':')[1].split()[0].strip()
238 assert querymax.isdigit(), \
239 "Number of reclaimable bytes not a number"
242 FatalError("Error in shrinking! "
243 "Couldn't find the max number of reclaimable bytes!")
245 querymax = int(querymax)
247 # Practically the smallest shrunken size generally is at around
248 # "used space" + (20-200 MB). Please also take into account that
249 # Windows might need about 50-100 MB free space left to boot safely.
250 # I'll give 100MB extra space just to be sure
254 self.out.warn("Not enought available space to shrink the image!")
258 r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
259 r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
260 'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
261 'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
262 r'ECHO EXIT >> %SCRIPT% & ' +
263 r'DISKPART /S %SCRIPT% & ' +
264 r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
267 stdout, stderr, rc = self._guest_exec(cmd)
269 for line in stdout.splitlines():
270 if line.find('shrunk') >= 0:
271 self.out.output(line)
273 def do_sysprep(self):
274 """Prepare system for image creation."""
276 if getattr(self, 'syspreped', False):
277 raise FatalError("Image is already syspreped!")
279 txt = "System preparation parameter: `%s' is needed but missing!"
280 for param in self.needed_sysprep_params():
281 if param[0] not in self.sysprep_params:
282 raise FatalError(txt % param[0])
284 self.mount(readonly=False)
286 disabled_uac = self._update_uac_remote_setting(1)
287 token = self._enable_os_monitor()
289 # disable the firewalls
290 firewall_states = self._update_firewalls(0, 0, 0)
292 # Delete the pagefile. It will be recreated when the system boots
293 systemroot = self.g.inspect_get_windows_systemroot(self.root)
294 pagefile = "%s/pagefile.sys" % systemroot
295 self.g.rm_rf(self.g.case_sensitive_path(pagefile))
300 self.out.output("Shutting down helper VM ...", False)
302 # guestfs_shutdown which is the prefered way to shutdown the backend
303 # process was introduced in version 1.19.16
304 if check_guestfs_version(self.g, 1, 19, 16) >= 0:
305 ret = self.g.shutdown()
307 ret = self.g.kill_subprocess()
309 self.out.success('done')
314 self.out.output("Starting windows VM ...", False)
315 monitorfd, monitor = tempfile.mkstemp()
317 vm, display = self._create_vm(monitor)
318 self.out.success("started (console on vnc display: %d)." % display)
320 self.out.output("Waiting for OS to boot ...", False)
321 if not self._wait_on_file(monitor, token):
322 raise FatalError("Windows booting timed out.")
324 time.sleep(10) # Just to be sure everything is up
325 self.out.success('done')
327 self.out.output("Disabling automatic logon ...", False)
328 self._disable_autologon()
329 self.out.success('done')
331 self.out.output('Preparing system from image creation:')
333 tasks = self.list_syspreps()
334 enabled = filter(lambda x: x.enabled, tasks)
337 # Make sure shrink runs in the end, before ms sysprep
338 enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
341 shrink_enabled = False
342 if len(enabled) != size:
343 enabled.append(self.shrink)
344 shrink_enabled = True
346 # Make sure the ms sysprep is the last task to run if it is enabled
348 lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
351 ms_sysprep_enabled = False
352 if len(enabled) != size:
353 enabled.append(self.microsoft_sysprep)
354 ms_sysprep_enabled = True
359 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
361 setattr(task.im_func, 'executed', True)
363 self.out.output("Sending shut down command ...", False)
364 if not ms_sysprep_enabled:
366 self.out.success("done")
368 self.out.output("Waiting for windows to shut down ...", False)
370 self.out.success("done")
372 if monitor is not None:
378 self.out.output("Relaunching helper VM (may take a while) ...",
381 self.out.success('done')
383 self.mount(readonly=False)
386 self._update_uac_remote_setting(0)
388 self._update_firewalls(*firewall_states)
392 def _create_vm(self, monitor):
393 """Create a VM with the image attached as the disk
395 monitor: a file to be used to monitor when the OS is up
399 mac = [0x00, 0x16, 0x3e,
400 random.randint(0x00, 0x7f),
401 random.randint(0x00, 0xff),
402 random.randint(0x00, 0xff)]
404 return ':'.join(map(lambda x: "%02x" % x, mac))
406 # Use ganeti's VNC port range for a random vnc port
407 vnc_port = random.randint(11000, 14999)
408 display = vnc_port - 5900
411 '-smp', '1', '-m', '1024', '-drive',
412 'file=%s,format=raw,cache=unsafe,if=virtio' % self.image.device,
413 '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
414 '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
415 '-vnc', ':%d' % display, '-serial', 'file:%s' % monitor, _bg=True)
419 def _destroy_vm(self, vm):
420 """Destroy a VM previously created by _create_vm"""
425 """Shuts down the windows VM"""
426 self._guest_exec(r'shutdown /s /t 5')
428 def _wait_on_file(self, fname, msg):
429 """Wait until a message appears on a file"""
431 for i in range(BOOT_TIMEOUT):
433 with open(fname) as f:
435 if line.startswith(msg):
439 def _disable_autologon(self):
440 """Disable automatic logon on the windows image"""
443 r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
445 self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
446 self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
447 self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
449 def _registry_file_path(self, regfile):
450 """Retrieves the case sensitive path to a registry file"""
452 systemroot = self.g.inspect_get_windows_systemroot(self.root)
453 path = "%s/system32/config/%s" % (systemroot, regfile)
455 path = self.g.case_sensitive_path(path)
456 except RuntimeError as e:
457 raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
461 def _enable_os_monitor(self):
462 """Add a script in the registry that will send a random string to the
463 first serial port when the windows image finishes booting.
466 token = "".join(random.choice(string.ascii_letters) for x in range(16))
468 path = self._registry_file_path('SOFTWARE')
469 softwarefd, software = tempfile.mkstemp()
472 self.g.download(path, software)
474 h = hivex.Hivex(software, write=True)
476 # Enable automatic logon.
477 # This is needed because we need to execute a script that we add in
478 # the RunOnce registry entry and those programs only get executed
479 # when a user logs on. There is a RunServicesOnce registry entry
480 # whose keys get executed in the background when the logon dialog
481 # box first appears, but they seem to only work with services and
482 # not arbitrary command line expressions :-(
484 # Instructions on how to turn on automatic logon in Windows can be
485 # found here: http://support.microsoft.com/kb/324737
487 # Warning: Registry change will not work if the “Logon Banner” is
488 # defined on the server either by a Group Policy object (GPO) or by
492 for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
494 winlogon = h.node_get_child(winlogon, child)
498 {'key': 'DefaultUserName', 't': 1,
499 'value': "Administrator".encode('utf-16le')})
502 {'key': 'DefaultPassword', 't': 1,
503 'value': self.sysprep_params['password'].encode('utf-16le')})
506 {'key': 'AutoAdminLogon', 't': 1,
507 'value': "1".encode('utf-16le')})
510 for child in ('Microsoft', 'Windows', 'CurrentVersion'):
511 key = h.node_get_child(key, child)
513 runonce = h.node_get_child(key, "RunOnce")
515 runonce = h.node_add_child(key, "RunOnce")
518 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
519 r'-ExecutionPolicy RemoteSigned '
520 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
521 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
522 r'$port.Close()}"').encode('utf-16le')
524 h.node_set_value(runonce,
525 {'key': "BootMonitor", 't': 1, 'value': value})
528 r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
529 r'\policies\system /v LocalAccountTokenFilterPolicy'
530 r' /t REG_DWORD /d 1 /f').encode('utf-16le')
532 h.node_set_value(runonce,
533 {'key': "UpdateRegistry", 't': 1, 'value': value})
537 self.g.upload(software, path)
543 def _update_firewalls(self, domain, public, standard):
544 """Enables or disables the firewall for the Domain, the Public and the
545 Standard profile. Returns a triplete with the old values.
547 1 will enable a firewall and 0 will disable it
550 if domain not in (0, 1):
551 raise ValueError("Valid values for domain parameter are 0 and 1")
553 if public not in (0, 1):
554 raise ValueError("Valid values for public parameter are 0 and 1")
556 if standard not in (0, 1):
557 raise ValueError("Valid values for standard parameter are 0 and 1")
559 path = self._registry_file_path("SYSTEM")
560 systemfd, system = tempfile.mkstemp()
563 self.g.download(path, system)
565 h = hivex.Hivex(system, write=True)
567 select = h.node_get_child(h.root(), 'Select')
568 current_value = h.node_get_value(select, 'Current')
570 # expecting a little endian dword
571 assert h.value_type(current_value)[1] == 4
572 current = "%03d" % h.value_dword(current_value)
574 firewall_policy = h.root()
575 for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
576 'Parameters', 'FirewallPolicy'):
577 firewall_policy = h.node_get_child(firewall_policy, child)
580 new_values = [domain, public, standard]
581 for profile in ('Domain', 'Public', 'Standard'):
582 node = h.node_get_child(firewall_policy, '%sProfile' % profile)
584 old_value = h.node_get_value(node, 'EnableFirewall')
586 # expecting a little endian dword
587 assert h.value_type(old_value)[1] == 4
588 old_values.append(h.value_dword(old_value))
591 node, {'key': 'EnableFirewall', 't': 4L,
592 'value': struct.pack("<I", new_values.pop(0))})
595 self.g.upload(system, path)
602 def _update_uac_remote_setting(self, value):
603 """Updates the registry key value:
604 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
605 \System]"LocalAccountTokenFilterPolicy"
607 value = 1 will disable the UAC remote restrictions
608 value = 0 will enable the UAC remote restrictions
610 For more info see here: http://support.microsoft.com/kb/951016
613 True if the key is changed
614 False if the key is unchanged
617 if value not in (0, 1):
618 raise ValueError("Valid values for value parameter are 0 and 1")
620 path = self._registry_file_path('SOFTWARE')
621 softwarefd, software = tempfile.mkstemp()
624 self.g.download(path, software)
626 h = hivex.Hivex(software, write=True)
629 for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
631 key = h.node_get_child(key, child)
634 for val in h.node_values(key):
635 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
638 if policy is not None:
639 dword = h.value_dword(policy)
645 new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
646 'value': struct.pack("<I", value)}
648 h.node_set_value(key, new_value)
651 self.g.upload(software, path)
658 def _do_collect_metadata(self):
659 """Collect metadata about the OS"""
660 super(Windows, self)._do_collect_metadata()
661 self.meta["USERS"] = " ".join(self._get_users())
663 def _get_users(self):
664 """Returns a list of users found in the images"""
665 path = self._registry_file_path('SAM')
666 samfd, sam = tempfile.mkstemp()
669 self.g.download(path, sam)
674 # Navigate to /SAM/Domains/Account/Users/Names
675 for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
676 key = h.node_get_child(key, child)
678 users = [h.node_name(x) for x in h.node_children(key)]
683 # Filter out the guest account
684 return filter(lambda x: x != "Guest", users)
686 def _guest_exec(self, command, fatal=True):
687 """Execute a command on a windows VM"""
689 passwd = self.sysprep_params['password']
691 winexe = WinEXE('Administrator', passwd, 'localhost')
692 winexe.runas('Administrator', passwd).uninstall()
695 (stdout, stderr, rc) = winexe.run(command)
696 except WinexeTimeout:
697 FatalError("Command: `%s' timeout out." % command)
699 if rc != 0 and fatal:
700 reason = stderr if len(stderr) else stdout
701 self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
702 (command, rc, reason))
703 raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
704 (command, rc, reason))
706 return (stdout, stderr, rc)
708 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :