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.
44 from image_creator import __version__ as version
45 from image_creator.util import FatalError, MD5
46 from image_creator.output import Output
47 from image_creator.output.cli import SimpleOutput
48 from image_creator.output.dialog import GaugeOutput, InfoBoxOutput
49 from image_creator.output.composite import CompositeOutput
50 from image_creator.disk import Disk
51 from image_creator.os_type import os_cls
52 from image_creator.kamaki_wrapper import Kamaki, ClientError
53 from image_creator.help import get_help_file
54 from image_creator.dialog_wizard import wizard
64 CONFIGURATION_TASKS = [
65 ("Partition table manipulation", ["FixPartitionTable"],
66 ["linux", "windows"]),
67 ("File system resize",
68 ["FilesystemResizeUnmounted", "FilesystemResizeMounted"],
69 ["linux", "windows"]),
70 ("Swap partition configuration", ["AddSwap"], ["linux"]),
71 ("SSH keys removal", ["DeleteSSHKeys"], ["linux"]),
72 ("Temporal RDP disabling", ["DisableRemoteDesktopConnections"],
74 ("SELinux relabeling at next boot", ["SELinuxAutorelabel"], ["linux"]),
75 ("Hostname/Computer Name assignment", ["AssignHostname"],
76 ["windows", "linux"]),
77 ("Password change", ["ChangePassword"], ["windows", "linux"]),
78 ("File injection", ["EnforcePersonality"], ["windows", "linux"])
82 class Reset(Exception):
86 class metadata_monitor(object):
87 def __init__(self, session, meta):
88 self.session = session
93 for (k, v) in self.meta.items():
96 def __exit__(self, type, value, traceback):
97 d = self.session['dialog']
102 for (k, v) in self.meta.items():
103 if k not in self.old:
105 elif self.old[k] != v:
108 if not (len(added) or len(altered)):
111 msg = "The last action has changed some image properties:\n\n"
113 msg += "New image properties:\n"
114 for (k, v) in added.items():
115 msg += ' %s: "%s"\n' % (k, v)
118 msg += "Updated image properties:\n"
119 for (k, v) in altered.items():
120 msg += ' %s: "%s" -> "%s"\n' % (k, self.old[k], v)
123 self.session['metadata'].update(added)
124 self.session['metadata'].update(altered)
125 d.msgbox(msg, title="Image Property Changes", width=MSGBOX_WIDTH)
128 def extract_metadata_string(session):
129 metadata = ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()]
131 if 'task_metadata' in session:
132 metadata.extend("%s=yes" % m for m in session['task_metadata'])
134 return '\n'.join(metadata) + '\n'
137 def confirm_exit(d, msg=''):
138 return not d.yesno("%s Do you want to exit?" % msg, width=YESNO_WIDTH)
141 def confirm_reset(d):
142 return not d.yesno("Are you sure you want to reset everything?",
143 width=YESNO_WIDTH, defaultno=1)
146 def update_background_title(session):
147 d = session['dialog']
148 dev = session['device']
152 size = (dev.size + MB - 1) // MB
153 shrinked = 'shrinked' in session and session['shrinked']
154 postfix = " (shrinked)" if shrinked else ''
156 title = "OS: %s, Distro: %s, Size: %dMB%s" % \
157 (dev.ostype, dev.distro, size, postfix)
159 d.setBackgroundTitle(title)
162 def extract_image(session):
163 d = session['dialog']
166 if dir and dir[-1] != os.sep:
169 (code, path) = d.fselect(dir, 10, 50, title="Save image as...")
170 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
173 if os.path.isdir(path):
177 if os.path.isdir("%s.meta" % path):
178 d.msgbox("Can't overwrite directory `%s.meta'" % path,
182 if os.path.isdir("%s.md5sum" % path):
183 d.msgbox("Can't overwrite directory `%s.md5sum'" % path,
187 basedir = os.path.dirname(path)
188 name = os.path.basename(path)
189 if not os.path.exists(basedir):
190 d.msgbox("Directory `%s' does not exist" % basedir,
198 files = ["%s%s" % (path, ext) for ext in ('', '.meta', '.md5sum')]
199 overwrite = filter(os.path.exists, files)
201 if len(overwrite) > 0:
202 if d.yesno("The following file(s) exist:\n"
203 "%s\nDo you want to overwrite them?" %
204 "\n".join(overwrite), width=YESNO_WIDTH):
207 gauge = GaugeOutput(d, "Image Extraction", "Extracting image...")
209 dev = session['device']
213 if "checksum" not in session:
216 session['checksum'] = md5.compute(session['snapshot'],
222 # Extract metadata file
223 out.output("Extracting metadata file...")
224 with open('%s.meta' % path, 'w') as f:
225 f.write(extract_metadata_string(session))
228 # Extract md5sum file
229 out.output("Extracting md5sum file...")
230 md5str = "%s %s\n" % (session['checksum'], name)
231 with open('%s.md5sum' % path, 'w') as f:
238 d.msgbox("Image file `%s' was successfully extracted!" % path,
245 def upload_image(session):
246 d = session["dialog"]
247 dev = session['device']
250 if "account" not in session:
251 d.msgbox("You need to provide your ~okeanos login username before you "
252 "can upload images to pithos+", width=MSGBOX_WIDTH)
255 if "token" not in session:
256 d.msgbox("You need to provide your ~okeanos account authentication "
257 "token before you can upload images to pithos+",
262 init = session["upload"] if "upload" in session else ''
263 (code, answer) = d.inputbox("Please provide a filename:", init=init,
264 width=INPUTBOX_WIDTH)
266 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
269 filename = answer.strip()
270 if len(filename) == 0:
271 d.msgbox("Filename cannot be empty", width=MSGBOX_WIDTH)
273 session['upload'] = filename
276 gauge = GaugeOutput(d, "Image Upload", "Uploading...")
281 if 'checksum' not in session:
283 session['checksum'] = md5.compute(session['snapshot'], size)
285 kamaki = Kamaki(session['account'], session['token'], out)
288 with open(session['snapshot'], 'rb') as f:
289 session["pithos_uri"] = kamaki.upload(f, size, filename,
290 "Calculating block hashes",
291 "Uploading missing blocks")
292 # Upload metadata file
293 out.output("Uploading metadata file...")
294 metastring = extract_metadata_string(session)
295 kamaki.upload(StringIO.StringIO(metastring),
296 size=len(metastring),
297 remote_path="%s.meta" % filename)
301 out.output("Uploading md5sum file...")
302 md5str = "%s %s\n" % (session['checksum'], filename)
303 kamaki.upload(StringIO.StringIO(md5str), size=len(md5str),
304 remote_path="%s.md5sum" % filename)
307 except ClientError as e:
308 d.msgbox("Error in pithos+ client: %s" % e.message,
309 title="Pithos+ Client Error", width=MSGBOX_WIDTH)
310 if 'pithos_uri' in session:
311 del session['pithos_uri']
318 d.msgbox("Image file `%s' was successfully uploaded to pithos+" % filename,
324 def register_image(session):
325 d = session["dialog"]
326 dev = session['device']
328 if "account" not in session:
329 d.msgbox("You need to provide your ~okeanos login username before you "
330 "can register an images to cyclades",
334 if "token" not in session:
335 d.msgbox("You need to provide your ~okeanos account authentication "
336 "token before you can register an images to cyclades",
340 if "pithos_uri" not in session:
341 d.msgbox("You need to upload the image to pithos+ before you can "
342 "register it to cyclades", width=MSGBOX_WIDTH)
346 (code, answer) = d.inputbox("Please provide a registration name:",
347 width=INPUTBOX_WIDTH)
348 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
351 name = answer.strip()
353 d.msgbox("Registration name cannot be empty", width=MSGBOX_WIDTH)
358 metadata.update(session['metadata'])
359 if 'task_metadata' in session:
360 for key in session['task_metadata']:
361 metadata[key] = 'yes'
363 gauge = GaugeOutput(d, "Image Registration", "Registrating image...")
368 out.output("Registring image to cyclades...")
370 kamaki = Kamaki(session['account'], session['token'], out)
371 kamaki.register(name, session['pithos_uri'], metadata)
373 except ClientError as e:
374 d.msgbox("Error in pithos+ client: %s" % e.message)
381 d.msgbox("Image `%s' was successfully registered to cyclades as `%s'" %
382 (session['upload'], name), width=MSGBOX_WIDTH)
386 def kamaki_menu(session):
387 d = session['dialog']
388 default_item = "Account"
390 account = Kamaki.get_account()
392 session['account'] = account
394 token = Kamaki.get_token()
396 session['token'] = token
399 account = session["account"] if "account" in session else "<none>"
400 token = session["token"] if "token" in session else "<none>"
401 upload = session["upload"] if "upload" in session else "<none>"
403 choices = [("Account", "Change your ~okeanos username: %s" % account),
404 ("Token", "Change your ~okeanos token: %s" % token),
405 ("Upload", "Upload image to pithos+"),
406 ("Register", "Register the image to cyclades: %s" % upload)]
408 (code, choice) = d.menu(
409 text="Choose one of the following or press <Back> to go back.",
410 width=MENU_WIDTH, choices=choices, cancel="Back", height=13,
411 menu_height=5, default_item=default_item,
412 title="Image Registration Menu")
414 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
417 if choice == "Account":
418 default_item = "Account"
419 (code, answer) = d.inputbox(
420 "Please provide your ~okeanos account e-mail address:",
421 init=session["account"] if "account" in session else '',
423 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
425 if len(answer) == 0 and "account" in session:
426 del session["account"]
428 session["account"] = answer.strip()
429 Kamaki.save_account(session['account'])
430 default_item = "Token"
431 elif choice == "Token":
432 default_item = "Token"
433 (code, answer) = d.inputbox(
434 "Please provide your ~okeanos account authetication token:",
435 init=session["token"] if "token" in session else '',
437 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
439 if len(answer) == 0 and "token" in session:
442 session["token"] = answer.strip()
443 Kamaki.save_token(session['token'])
444 default_item = "Upload"
445 elif choice == "Upload":
446 if upload_image(session):
447 default_item = "Register"
449 default_item = "Upload"
450 elif choice == "Register":
451 if register_image(session):
454 default_item = "Register"
457 def add_property(session):
458 d = session['dialog']
461 (code, answer) = d.inputbox("Please provide a name for a new image"
462 " property:", width=INPUTBOX_WIDTH)
463 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
466 name = answer.strip()
468 d.msgbox("A property name cannot be empty", width=MSGBOX_WIDTH)
474 (code, answer) = d.inputbox("Please provide a value for image "
475 "property %s" % name, width=INPUTBOX_WIDTH)
476 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
479 value = answer.strip()
481 d.msgbox("Value cannot be empty", width=MSGBOX_WIDTH)
486 session['metadata'][name] = value
491 def modify_properties(session):
492 d = session['dialog']
496 for (key, val) in session['metadata'].items():
497 choices.append((str(key), str(val)))
499 (code, choice) = d.menu(
500 "In this menu you can edit existing image properties or add new "
501 "ones. Be careful! Most properties have special meaning and "
502 "alter the image deployment behaviour. Press <HELP> to see more "
503 "information about image properties. Press <BACK> when done.",
504 height=18, width=MENU_WIDTH, choices=choices, menu_height=10,
505 ok_label="Edit", extra_button=1, extra_label="Add", cancel="Back",
506 help_button=1, title="Image Properties")
508 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
511 elif code == d.DIALOG_OK:
512 (code, answer) = d.inputbox("Please provide a new value for the "
513 "image property with name `%s':" %
515 init=session['metadata'][choice],
516 width=INPUTBOX_WIDTH)
517 if code not in (d.DIALOG_CANCEL, d.DIALOG_ESC):
518 value = answer.strip()
520 d.msgbox("Value cannot be empty!")
523 session['metadata'][choice] = value
525 elif code == d.DIALOG_EXTRA:
526 add_property(session)
528 help_file = get_help_file("image_properties")
529 assert os.path.exists(help_file)
530 d.textbox(help_file, title="Image Properties", width=70, height=40)
533 def delete_properties(session):
534 d = session['dialog']
537 for (key, val) in session['metadata'].items():
538 choices.append((key, "%s" % val, 0))
540 (code, to_delete) = d.checklist("Choose which properties to delete:",
541 choices=choices, width=CHECKBOX_WIDTH)
543 # If the user exits with ESC or CANCEL, the returned tag list is empty.
545 del session['metadata'][i]
549 d.msgbox("%d image properties were deleted." % cnt, width=MSGBOX_WIDTH)
555 def exclude_tasks(session):
556 d = session['dialog']
562 if 'excluded_tasks' not in session:
563 session['excluded_tasks'] = []
565 if -1 in session['excluded_tasks']:
566 if not d.yesno("Image deployment configuration is disabled. "
567 "Do you wish to enable it?", width=YESNO_WIDTH):
568 session['excluded_tasks'].remove(-1)
572 for (msg, task, osfamily) in CONFIGURATION_TASKS:
573 if session['metadata']['OSFAMILY'] in osfamily:
574 checked = 1 if index in session['excluded_tasks'] else 0
575 choices.append((str(displayed_index), msg, checked))
576 mapping[displayed_index] = index
581 (code, tags) = d.checklist(
582 text="Please choose which configuration tasks you would like to "
583 "prevent from running during image deployment. "
584 "Press <No Config> to supress any configuration. "
585 "Press <Help> for more help on the image deployment "
586 "configuration tasks.",
587 choices=choices, height=19, list_height=8, width=CHECKBOX_WIDTH,
588 help_button=1, extra_button=1, extra_label="No Config",
589 title="Exclude Configuration Tasks")
591 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
593 elif code == d.DIALOG_HELP:
594 help_file = get_help_file("configuration_tasks")
595 assert os.path.exists(help_file)
596 d.textbox(help_file, title="Configuration Tasks",
599 elif code == d.DIALOG_EXTRA:
600 session['excluded_tasks'] = [-1]
601 session['task_metadata'] = ["EXCLUDE_ALL_TASKS"]
603 elif code == d.DIALOG_OK:
604 session['excluded_tasks'] = []
606 session['excluded_tasks'].append(mapping[int(tag)])
608 exclude_metadata = []
609 for task in session['excluded_tasks']:
610 exclude_metadata.extend(CONFIGURATION_TASKS[task][1])
612 session['task_metadata'] = map(lambda x: "EXCLUDE_TASK_%s" % x,
619 def sysprep(session):
620 d = session['dialog']
621 image_os = session['image_os']
623 # Is the image already shrinked?
624 if 'shrinked' in session and session['shrinked']:
625 msg = "It seems you have shrinked the image. Running system " \
626 "preparation tasks on a shrinked image is dangerous."
628 if d.yesno("%s\n\nDo you really want to continue?" % msg,
629 width=YESNO_WIDTH, defaultno=1):
632 wrapper = textwrap.TextWrapper(width=65)
634 help_title = "System Preperation Tasks"
635 sysprep_help = "%s\n%s\n\n" % (help_title, '=' * len(help_title))
637 if 'exec_syspreps' not in session:
638 session['exec_syspreps'] = []
640 all_syspreps = image_os.list_syspreps()
641 # Only give the user the choice between syspreps that have not ran yet
642 syspreps = [s for s in all_syspreps if s not in session['exec_syspreps']]
644 if len(syspreps) == 0:
645 d.msgbox("No system preparation task available to run!",
646 title="System Preperation", width=MSGBOX_WIDTH)
652 for sysprep in syspreps:
653 name, descr = image_os.sysprep_info(sysprep)
654 display_name = name.replace('-', ' ').capitalize()
655 sysprep_help += "%s\n" % display_name
656 sysprep_help += "%s\n" % ('-' * len(display_name))
657 sysprep_help += "%s\n\n" % wrapper.fill(" ".join(descr.split()))
658 enabled = 1 if sysprep.enabled else 0
659 choices.append((str(index + 1), display_name, enabled))
662 (code, tags) = d.checklist(
663 "Please choose which system preperation tasks you would like to "
664 "run on the image. Press <Help> to see details about the system "
665 "preperation tasks.", title="Run system preperation tasks",
666 choices=choices, width=70, ok_label="Run", help_button=1)
668 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
670 elif code == d.DIALOG_HELP:
671 d.scrollbox(sysprep_help, width=HELP_WIDTH)
672 elif code == d.DIALOG_OK:
673 # Enable selected syspreps and disable the rest
674 for i in range(len(syspreps)):
675 if str(i + 1) in tags:
676 image_os.enable_sysprep(syspreps[i])
677 session['exec_syspreps'].append(syspreps[i])
679 image_os.disable_sysprep(syspreps[i])
681 infobox = InfoBoxOutput(d, "Image Configuration")
683 dev = session['device']
686 dev.mount(readonly=False)
688 # The checksum is invalid. We have mounted the image rw
689 if 'checksum' in session:
690 del session['checksum']
692 # Monitor the metadata changes during syspreps
693 with metadata_monitor(session, image_os.meta):
694 image_os.do_sysprep()
697 # Disable syspreps that have ran
698 for sysprep in session['exec_syspreps']:
699 image_os.disable_sysprep(sysprep)
703 dev.out.remove(infobox)
711 d = session['dialog']
712 dev = session['device']
714 shrinked = 'shrinked' in session and session['shrinked']
717 d.msgbox("The image is already shrinked!", title="Image Shrinking",
721 msg = "This operation will shrink the last partition of the image to " \
722 "reduce the total image size. If the last partition is a swap " \
723 "partition, then this partition is removed and the partition " \
724 "before that is shrinked. The removed swap partition will be " \
725 "recreated during image deployment."
727 if not d.yesno("%s\n\nDo you want to continue?" % msg, width=70,
728 height=12, title="Image Shrinking"):
729 with metadata_monitor(session, dev.meta):
730 infobox = InfoBoxOutput(d, "Image Shrinking", height=4)
736 dev.out.remove(infobox)
738 session['shrinked'] = True
739 update_background_title(session)
746 def customization_menu(session):
747 d = session['dialog']
749 choices = [("Sysprep", "Run various image preperation tasks"),
750 ("Shrink", "Shrink image"),
751 ("View/Modify", "View/Modify image properties"),
752 ("Delete", "Delete image properties"),
753 ("Exclude", "Exclude various deployment tasks from running")]
757 actions = {"Sysprep": sysprep,
759 "View/Modify": modify_properties,
760 "Delete": delete_properties,
761 "Exclude": exclude_tasks}
763 (code, choice) = d.menu(
764 text="Choose one of the following or press <Back> to exit.",
765 width=MENU_WIDTH, choices=choices, cancel="Back", height=13,
766 menu_height=len(choices), default_item=choices[default_item][0],
767 title="Image Customization Menu")
769 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
771 elif choice in actions:
772 default_item = [entry[0] for entry in choices].index(choice)
773 if actions[choice](session):
774 default_item = (default_item + 1) % len(choices)
777 def main_menu(session):
778 d = session['dialog']
779 dev = session['device']
781 update_background_title(session)
783 choices = [("Customize", "Customize image & ~okeanos deployment options"),
784 ("Register", "Register image to ~okeanos"),
785 ("Extract", "Dump image to local file system"),
786 ("Reset", "Reset everything and start over again"),
787 ("Help", "Get help for using snf-image-creator")]
789 default_item = "Customize"
791 actions = {"Customize": customization_menu, "Register": kamaki_menu,
792 "Extract": extract_image}
794 (code, choice) = d.menu(
795 text="Choose one of the following or press <Exit> to exit.",
796 width=MENU_WIDTH, choices=choices, cancel="Exit", height=13,
797 default_item=default_item, menu_height=len(choices),
798 title="Image Creator for ~okeanos (snf-image-creator version %s)" %
801 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
804 elif choice == "Reset":
806 d.infobox("Resetting snf-image-creator. Please wait...",
809 elif choice in actions:
810 actions[choice](session)
813 def image_creator(d, media, out):
815 d.setBackgroundTitle('snf-image-creator')
817 gauge = GaugeOutput(d, "Initialization", "Initializing...")
819 disk = Disk(media, out)
821 def signal_handler(signum, frame):
825 signal.signal(signal.SIGINT, signal_handler)
827 snapshot = disk.snapshot()
828 dev = disk.get_device(snapshot)
831 for (key, value) in dev.meta.items():
832 metadata[str(key)] = str(value)
834 dev.mount(readonly=True)
835 out.output("Collecting image metadata...")
836 cls = os_cls(dev.distro, dev.ostype)
837 image_os = cls(dev.root, dev.g, out)
840 for (key, value) in image_os.meta.items():
841 metadata[str(key)] = str(value)
847 # Make sure the signal handler does not call gauge.cleanup again
850 gauge.cleanup = type(GaugeOutput.cleanup)(dummy, gauge, GaugeOutput)
852 session = {"dialog": d,
854 "snapshot": snapshot,
856 "image_os": image_os,
857 "metadata": metadata}
859 msg = "snf-image-creator detected a %s system on the input media. " \
860 "Would you like to run a wizard to assist you through the " \
861 "image creation process?\n\nChoose <Wizard> to run the wizard," \
862 " <Expert> to run the snf-image-creator in expert mode or press " \
863 "ESC to quit the program." \
864 % (dev.ostype if dev.ostype == dev.distro else "%s (%s)" %
865 (dev.ostype, dev.distro))
867 update_background_title(session)
870 code = d.yesno(msg, width=YESNO_WIDTH, height=12,
871 yes_label="Wizard", no_label="Expert")
872 if code == d.DIALOG_OK:
875 elif code == d.DIALOG_CANCEL:
882 d.infobox("Thank you for using snf-image-creator. Bye", width=53)
889 def select_file(d, media):
892 if media is not None:
893 if not os.path.exists(media):
894 d.msgbox("The file `%s' you choose does not exist." % media,
899 (code, media) = d.fselect(root, 10, 50,
900 title="Please select input media")
901 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
902 if confirm_exit(d, "You canceled the media selection dialog box."):
913 d = dialog.Dialog(dialog="dialog")
915 # Add extra button in dialog library
916 dialog._common_args_syntax["extra_button"] = \
917 lambda enable: dialog._simple_option("--extra-button", enable)
919 dialog._common_args_syntax["extra_label"] = \
920 lambda string: ("--extra-label", string)
922 # Allow yes-no label overwriting
923 dialog._common_args_syntax["yes_label"] = \
924 lambda string: ("--yes-label", string)
926 dialog._common_args_syntax["no_label"] = \
927 lambda string: ("--no-label", string)
929 usage = "Usage: %prog [options] [<input_media>]"
930 parser = optparse.OptionParser(version=version, usage=usage)
931 parser.add_option("-l", "--logfile", type="string", dest="logfile",
932 default=None, help="log all messages to FILE",
935 options, args = parser.parse_args(sys.argv[1:])
938 parser.error("Wrong number of arguments")
940 d.setBackgroundTitle('snf-image-creator')
943 if os.geteuid() != 0:
944 raise FatalError("You must run %s as root" % \
945 parser.get_prog_name())
947 media = select_file(d, args[0] if len(args) == 1 else None)
950 if options.logfile is not None:
952 logfile = open(options.logfile, 'w')
955 "Unable to open logfile `%s' for writing. Reason: %s" % \
956 (options.logfile, e.strerror))
958 log = SimpleOutput(False, logfile) if logfile is not None \
962 out = CompositeOutput([log])
963 out.output("Starting %s v%s..." % \
964 (parser.get_prog_name(), version))
965 ret = image_creator(d, media, out)
968 log.output("Resetting everything...")
971 if logfile is not None:
973 except FatalError as e:
974 msg = textwrap.fill(str(e), width=70)
975 d.infobox(msg, width=INFOBOX_WIDTH, title="Fatal Error")
978 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :