Update version.py and ChangeLog for 0.6.1
[snf-image-creator] / image_creator / dialog_menu.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 implements the "expert" mode of the dialog-based version of
37 snf-image-creator.
38 """
39
40 import os
41 import textwrap
42 import StringIO
43 import json
44
45 from image_creator import __version__ as version
46 from image_creator.util import MD5, FatalError
47 from image_creator.output.dialog import GaugeOutput, InfoBoxOutput
48 from image_creator.kamaki_wrapper import Kamaki, ClientError
49 from image_creator.help import get_help_file
50 from image_creator.dialog_util import SMALL_WIDTH, WIDTH, \
51     update_background_title, confirm_reset, confirm_exit, Reset, \
52     extract_image, extract_metadata_string, add_cloud, edit_cloud
53
54 CONFIGURATION_TASKS = [
55     ("Partition table manipulation", ["FixPartitionTable"],
56         ["linux", "windows"]),
57     ("File system resize",
58         ["FilesystemResizeUnmounted", "FilesystemResizeMounted"],
59         ["linux", "windows"]),
60     ("Swap partition configuration", ["AddSwap"], ["linux"]),
61     ("SSH keys removal", ["DeleteSSHKeys"], ["linux"]),
62     ("Temporal RDP disabling", ["DisableRemoteDesktopConnections"],
63         ["windows"]),
64     ("SELinux relabeling at next boot", ["SELinuxAutorelabel"], ["linux"]),
65     ("Hostname/Computer Name assignment", ["AssignHostname"],
66         ["windows", "linux"]),
67     ("Password change", ["ChangePassword"], ["windows", "linux"]),
68     ("File injection", ["EnforcePersonality"], ["windows", "linux"])
69 ]
70
71 SYSPREP_PARAM_MAXLEN = 20
72
73
74 class MetadataMonitor(object):
75     """Monitors image metadata chages"""
76     def __init__(self, session, meta):
77         self.session = session
78         self.meta = meta
79
80     def __enter__(self):
81         self.old = {}
82         for (k, v) in self.meta.items():
83             self.old[k] = v
84
85     def __exit__(self, type, value, traceback):
86         d = self.session['dialog']
87
88         altered = {}
89         added = {}
90
91         for (k, v) in self.meta.items():
92             if k not in self.old:
93                 added[k] = v
94             elif self.old[k] != v:
95                 altered[k] = v
96
97         if not (len(added) or len(altered)):
98             return
99
100         msg = "The last action has changed some image properties:\n\n"
101         if len(added):
102             msg += "New image properties:\n"
103             for (k, v) in added.items():
104                 msg += '    %s: "%s"\n' % (k, v)
105             msg += "\n"
106         if len(altered):
107             msg += "Updated image properties:\n"
108             for (k, v) in altered.items():
109                 msg += '    %s: "%s" -> "%s"\n' % (k, self.old[k], v)
110             msg += "\n"
111
112         self.session['metadata'].update(added)
113         self.session['metadata'].update(altered)
114         d.msgbox(msg, title="Image Property Changes", width=SMALL_WIDTH)
115
116
117 def upload_image(session):
118     """Upload the image to the storage service"""
119     d = session["dialog"]
120     image = session['image']
121     meta = session['metadata']
122     size = image.size
123
124     if "account" not in session:
125         d.msgbox("You need to select a valid cloud before you can upload "
126                  "images to it", width=SMALL_WIDTH)
127         return False
128
129     while 1:
130         if 'upload' in session:
131             init = session['upload']
132         elif 'OS' in meta:
133             init = "%s.diskdump" % meta['OS']
134         else:
135             init = ""
136         (code, answer) = d.inputbox("Please provide a filename:", init=init,
137                                     width=WIDTH)
138
139         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
140             return False
141
142         filename = answer.strip()
143         if len(filename) == 0:
144             d.msgbox("Filename cannot be empty", width=SMALL_WIDTH)
145             continue
146
147         kamaki = Kamaki(session['account'], None)
148         overwrite = []
149         for f in (filename, "%s.md5sum" % filename, "%s.meta" % filename):
150             if kamaki.object_exists(f):
151                 overwrite.append(f)
152
153         if len(overwrite) > 0:
154             if d.yesno("The following storage service object(s) already "
155                        "exist(s):\n%s\nDo you want to overwrite them?" %
156                        "\n".join(overwrite), width=WIDTH, defaultno=1):
157                 continue
158
159         session['upload'] = filename
160         break
161
162     gauge = GaugeOutput(d, "Image Upload", "Uploading ...")
163     try:
164         out = image.out
165         out.add(gauge)
166         kamaki.out = out
167         try:
168             if 'checksum' not in session:
169                 md5 = MD5(out)
170                 session['checksum'] = md5.compute(image.device, size)
171
172             try:
173                 # Upload image file
174                 with open(image.device, 'rb') as f:
175                     session["pithos_uri"] = \
176                         kamaki.upload(f, size, filename,
177                                       "Calculating block hashes",
178                                       "Uploading missing blocks")
179                 # Upload md5sum file
180                 out.output("Uploading md5sum file ...")
181                 md5str = "%s %s\n" % (session['checksum'], filename)
182                 kamaki.upload(StringIO.StringIO(md5str), size=len(md5str),
183                               remote_path="%s.md5sum" % filename)
184                 out.success("done")
185
186             except ClientError as e:
187                 d.msgbox(
188                     "Error in storage service client: %s" % e.message,
189                     title="Storage Service Client Error", width=SMALL_WIDTH)
190                 if 'pithos_uri' in session:
191                     del session['pithos_uri']
192                 return False
193         finally:
194             out.remove(gauge)
195     finally:
196         gauge.cleanup()
197
198     d.msgbox("Image file `%s' was successfully uploaded" % filename,
199              width=SMALL_WIDTH)
200
201     return True
202
203
204 def register_image(session):
205     """Register image with the compute service"""
206     d = session["dialog"]
207
208     is_public = False
209
210     if "account" not in session:
211         d.msgbox("You need to select a valid cloud before you "
212                  "can register an images with it", width=SMALL_WIDTH)
213         return False
214
215     if "pithos_uri" not in session:
216         d.msgbox("You need to upload the image to the cloud before you can "
217                  "register it", width=SMALL_WIDTH)
218         return False
219
220     name = ""
221     description = session['metadata']['DESCRIPTION'] if 'DESCRIPTION' in \
222         session['metadata'] else ""
223
224     while 1:
225         fields = [
226             ("Registration name:", name, 60),
227             ("Description (optional):", description, 80)]
228
229         (code, output) = d.form(
230             "Please provide the following registration info:", height=11,
231             width=WIDTH, form_height=2, fields=fields)
232
233         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
234             return False
235
236         name, description = output
237         name = name.strip()
238         description = description.strip()
239
240         if len(name) == 0:
241             d.msgbox("Registration name cannot be empty", width=SMALL_WIDTH)
242             continue
243
244         ret = d.yesno("Make the image public?\\nA public image is accessible "
245                       "by every user of the service.", defaultno=1,
246                       width=WIDTH)
247         if ret not in (0, 1):
248             continue
249
250         is_public = True if ret == 0 else False
251
252         break
253
254     session['metadata']['DESCRIPTION'] = description
255     metadata = {}
256     metadata.update(session['metadata'])
257     if 'task_metadata' in session:
258         for key in session['task_metadata']:
259             metadata[key] = 'yes'
260
261     img_type = "public" if is_public else "private"
262     gauge = GaugeOutput(d, "Image Registration", "Registering image ...")
263     try:
264         out = session['image'].out
265         out.add(gauge)
266         try:
267             try:
268                 out.output("Registering %s image with the cloud ..." %
269                            img_type)
270                 kamaki = Kamaki(session['account'], out)
271                 result = kamaki.register(name, session['pithos_uri'], metadata,
272                                          is_public)
273                 out.success('done')
274                 # Upload metadata file
275                 out.output("Uploading metadata file ...")
276                 metastring = unicode(json.dumps(result, ensure_ascii=False))
277                 kamaki.upload(StringIO.StringIO(metastring),
278                               size=len(metastring),
279                               remote_path="%s.meta" % session['upload'])
280                 out.success("done")
281                 if is_public:
282                     out.output("Sharing metadata and md5sum files ...")
283                     kamaki.share("%s.meta" % session['upload'])
284                     kamaki.share("%s.md5sum" % session['upload'])
285                     out.success('done')
286             except ClientError as e:
287                 d.msgbox("Error in storage service client: %s" % e.message)
288                 return False
289         finally:
290             out.remove(gauge)
291     finally:
292         gauge.cleanup()
293
294     d.msgbox("%s image `%s' was successfully registered with the cloud as `%s'"
295              % (img_type.title(), session['upload'], name), width=SMALL_WIDTH)
296     return True
297
298
299 def modify_clouds(session):
300     """Modify existing cloud accounts"""
301     d = session['dialog']
302
303     while 1:
304         clouds = Kamaki.get_clouds()
305         if not len(clouds):
306             if not add_cloud(session):
307                 break
308             continue
309
310         choices = []
311         for (name, cloud) in clouds.items():
312             descr = cloud['description'] if 'description' in cloud else ''
313             choices.append((name, descr))
314
315         (code, choice) = d.menu(
316             "In this menu you can edit existing cloud accounts or add new "
317             " ones. Press <Edit> to edit an existing account or <Add> to add "
318             " a new one. Press <Back> or hit <ESC> when done.", height=18,
319             width=WIDTH, choices=choices, menu_height=10, ok_label="Edit",
320             extra_button=1, extra_label="Add", cancel="Back", help_button=1,
321             title="Clouds")
322
323         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
324             return True
325         elif code == d.DIALOG_OK:  # Edit button
326             edit_cloud(session, choice)
327         elif code == d.DIALOG_EXTRA:  # Add button
328             add_cloud(session)
329
330
331 def delete_clouds(session):
332     """Delete existing cloud accounts"""
333     d = session['dialog']
334
335     choices = []
336     for (name, cloud) in Kamaki.get_clouds().items():
337         descr = cloud['description'] if 'description' in cloud else ''
338         choices.append((name, descr, 0))
339
340     if len(choices) == 0:
341         d.msgbox("No available clouds to delete!", width=SMALL_WIDTH)
342         return True
343
344     (code, to_delete) = d.checklist("Choose which cloud accounts to delete:",
345                                     choices=choices, width=WIDTH)
346     to_delete = map(lambda x: x.strip('"'), to_delete)  # Needed for OpenSUSE
347
348     if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
349         return False
350
351     if not len(to_delete):
352         d.msgbox("Nothing selected!", width=SMALL_WIDTH)
353         return False
354
355     if not d.yesno("Are you sure you want to remove the selected cloud "
356                    "accounts?", width=WIDTH, defaultno=1):
357         for i in to_delete:
358             Kamaki.remove_cloud(i)
359             if 'cloud' in session and session['cloud'] == i:
360                 del session['cloud']
361                 if 'account' in session:
362                     del session['account']
363     else:
364         return False
365
366     d.msgbox("%d cloud accounts were deleted." % len(to_delete),
367              width=SMALL_WIDTH)
368     return True
369
370
371 def kamaki_menu(session):
372     """Show kamaki related actions"""
373     d = session['dialog']
374     default_item = "Cloud"
375
376     if 'cloud' not in session:
377         cloud = Kamaki.get_default_cloud_name()
378         if cloud:
379             session['cloud'] = cloud
380             session['account'] = Kamaki.get_account(cloud)
381             if not session['account']:
382                 del session['account']
383         else:
384             default_item = "Add/Edit"
385
386     while 1:
387         cloud = session["cloud"] if "cloud" in session else "<none>"
388         if 'account' not in session and 'cloud' in session:
389             cloud += " <invalid>"
390
391         upload = session["upload"] if "upload" in session else "<none>"
392
393         choices = [("Add/Edit", "Add/Edit cloud accounts"),
394                    ("Delete", "Delete existing cloud accounts"),
395                    ("Cloud", "Select cloud account to use: %s" % cloud),
396                    ("Upload", "Upload image to the cloud"),
397                    ("Register", "Register image with the cloud: %s" % upload)]
398
399         (code, choice) = d.menu(
400             text="Choose one of the following or press <Back> to go back.",
401             width=WIDTH, choices=choices, cancel="Back", height=13,
402             menu_height=5, default_item=default_item,
403             title="Image Registration Menu")
404
405         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
406             return False
407
408         if choice == "Add/Edit":
409             if modify_clouds(session):
410                 default_item = "Cloud"
411         elif choice == "Delete":
412             if delete_clouds(session):
413                 if len(Kamaki.get_clouds()):
414                     default_item = "Cloud"
415                 else:
416                     default_item = "Add/Edit"
417             else:
418                 default_item = "Delete"
419         elif choice == "Cloud":
420             default_item = "Cloud"
421             clouds = Kamaki.get_clouds()
422             if not len(clouds):
423                 d.msgbox("No clouds available. Please add a new cloud!",
424                          width=SMALL_WIDTH)
425                 default_item = "Add/Edit"
426                 continue
427
428             if 'cloud' not in session:
429                 session['cloud'] = clouds.keys()[0]
430
431             choices = []
432             for name, info in clouds.items():
433                 default = 1 if session['cloud'] == name else 0
434                 descr = info['description'] if 'description' in info else ""
435                 choices.append((name, descr, default))
436
437             (code, answer) = d.radiolist("Please select a cloud:",
438                                          width=WIDTH, choices=choices)
439             if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
440                 continue
441             else:
442                 session['account'] = Kamaki.get_account(answer)
443
444                 if session['account'] is None:  # invalid account
445                     if not d.yesno("The cloud %s' is not valid! Would you "
446                                    "like to edit it?" % answer, width=WIDTH):
447                         if edit_cloud(session, answer):
448                             session['account'] = Kamaki.get_account(answer)
449                             Kamaki.set_default_cloud(answer)
450
451                 if session['account'] is not None:
452                     session['cloud'] = answer
453                     Kamaki.set_default_cloud(answer)
454                     default_item = "Upload"
455                 else:
456                     del session['account']
457                     del session['cloud']
458         elif choice == "Upload":
459             if upload_image(session):
460                 default_item = "Register"
461             else:
462                 default_item = "Upload"
463         elif choice == "Register":
464             if register_image(session):
465                 return True
466             else:
467                 default_item = "Register"
468
469
470 def add_property(session):
471     """Add a new property to the image"""
472     d = session['dialog']
473
474     while 1:
475         (code, answer) = d.inputbox("Please provide a name for a new image"
476                                     " property:", width=WIDTH)
477         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
478             return False
479
480         name = answer.strip()
481         if len(name) == 0:
482             d.msgbox("A property name cannot be empty", width=SMALL_WIDTH)
483             continue
484
485         break
486
487     while 1:
488         (code, answer) = d.inputbox("Please provide a value for image "
489                                     "property %s" % name, width=WIDTH)
490         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
491             return False
492
493         value = answer.strip()
494         if len(value) == 0:
495             d.msgbox("Value cannot be empty", width=SMALL_WIDTH)
496             continue
497
498         break
499
500     session['metadata'][name] = value
501
502     return True
503
504
505 def show_properties_help(session):
506     """Show help for image properties"""
507     d = session['dialog']
508
509     help_file = get_help_file("image_properties")
510     assert os.path.exists(help_file)
511     d.textbox(help_file, title="Image Properties", width=70, height=40)
512
513
514 def modify_properties(session):
515     """Modify an existing image property"""
516     d = session['dialog']
517
518     while 1:
519         choices = []
520         for (key, val) in session['metadata'].items():
521             choices.append((str(key), str(val)))
522
523         if len(choices) == 0:
524             code = d.yesno(
525                 "No image properties are available. "
526                 "Would you like to add a new one?", width=WIDTH, help_button=1)
527             if code == d.DIALOG_OK:
528                 if not add_property(session):
529                     return True
530             elif code == d.DIALOG_CANCEL:
531                 return True
532             elif code == d.DIALOG_HELP:
533                 show_properties_help(session)
534             continue
535
536         (code, choice) = d.menu(
537             "In this menu you can edit existing image properties or add new "
538             "ones. Be careful! Most properties have special meaning and "
539             "alter the image deployment behaviour. Press <HELP> to see more "
540             "information about image properties. Press <BACK> when done.",
541             height=18, width=WIDTH, choices=choices, menu_height=10,
542             ok_label="Edit", extra_button=1, extra_label="Add", cancel="Back",
543             help_button=1, title="Image Properties")
544
545         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
546             return True
547         # Edit button
548         elif code == d.DIALOG_OK:
549             (code, answer) = d.inputbox("Please provide a new value for the "
550                                         "image property with name `%s':" %
551                                         choice,
552                                         init=session['metadata'][choice],
553                                         width=WIDTH)
554             if code not in (d.DIALOG_CANCEL, d.DIALOG_ESC):
555                 value = answer.strip()
556                 if len(value) == 0:
557                     d.msgbox("Value cannot be empty!")
558                     continue
559                 else:
560                     session['metadata'][choice] = value
561         # ADD button
562         elif code == d.DIALOG_EXTRA:
563             add_property(session)
564         elif code == 'help':
565             show_properties_help(session)
566
567
568 def delete_properties(session):
569     """Delete an image property"""
570     d = session['dialog']
571
572     choices = []
573     for (key, val) in session['metadata'].items():
574         choices.append((key, "%s" % val, 0))
575
576     if len(choices) == 0:
577         d.msgbox("No available images properties to delete!",
578                  width=SMALL_WIDTH)
579         return True
580
581     (code, to_delete) = d.checklist("Choose which properties to delete:",
582                                     choices=choices, width=WIDTH)
583     to_delete = map(lambda x: x.strip('"'), to_delete)  # needed for OpenSUSE
584
585     # If the user exits with ESC or CANCEL, the returned tag list is empty.
586     for i in to_delete:
587         del session['metadata'][i]
588
589     cnt = len(to_delete)
590     if cnt > 0:
591         d.msgbox("%d image properties were deleted." % cnt, width=SMALL_WIDTH)
592         return True
593     else:
594         return False
595
596
597 def exclude_tasks(session):
598     """Exclude specific tasks from running during image deployment"""
599     d = session['dialog']
600     image = session['image']
601
602     if image.is_unsupported():
603         d.msgbox("Image deployment configuration is disabled for unsupported "
604                  "images.", width=SMALL_WIDTH)
605         return False
606
607     index = 0
608     displayed_index = 1
609     choices = []
610     mapping = {}
611     if 'excluded_tasks' not in session:
612         session['excluded_tasks'] = []
613
614     if -1 in session['excluded_tasks']:
615         if not d.yesno("Image deployment configuration is disabled. "
616                        "Do you wish to enable it?", width=SMALL_WIDTH):
617             session['excluded_tasks'].remove(-1)
618         else:
619             return False
620
621     for (msg, task, osfamily) in CONFIGURATION_TASKS:
622         if session['metadata']['OSFAMILY'] in osfamily:
623             checked = 1 if index in session['excluded_tasks'] else 0
624             choices.append((str(displayed_index), msg, checked))
625             mapping[displayed_index] = index
626             displayed_index += 1
627         index += 1
628
629     while 1:
630         (code, tags) = d.checklist(
631             text="Please choose which configuration tasks you would like to "
632                  "prevent from running during image deployment. "
633                  "Press <No Config> to supress any configuration. "
634                  "Press <Help> for more help on the image deployment "
635                  "configuration tasks.",
636             choices=choices, height=19, list_height=8, width=WIDTH,
637             help_button=1, extra_button=1, extra_label="No Config",
638             title="Exclude Configuration Tasks")
639         tags = map(lambda x: x.strip('"'), tags)  # Needed for OpenSUSE
640
641         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
642             return False
643         elif code == d.DIALOG_HELP:
644             help_file = get_help_file("configuration_tasks")
645             assert os.path.exists(help_file)
646             d.textbox(help_file, title="Configuration Tasks",
647                       width=70, height=40)
648         # No Config button
649         elif code == d.DIALOG_EXTRA:
650             session['excluded_tasks'] = [-1]
651             session['task_metadata'] = ["EXCLUDE_ALL_TASKS"]
652             break
653         elif code == d.DIALOG_OK:
654             session['excluded_tasks'] = []
655             for tag in tags:
656                 session['excluded_tasks'].append(mapping[int(tag)])
657
658             exclude_metadata = []
659             for task in session['excluded_tasks']:
660                 exclude_metadata.extend(CONFIGURATION_TASKS[task][1])
661
662             session['task_metadata'] = map(lambda x: "EXCLUDE_TASK_%s" % x,
663                                            exclude_metadata)
664             break
665
666     return True
667
668
669 def sysprep_params(session):
670     """Collect the needed sysprep parameters"""
671     d = session['dialog']
672     image = session['image']
673
674     available = image.os.sysprep_params
675     needed = image.os.needed_sysprep_params
676
677     if len(needed) == 0:
678         return True
679
680     def print_form(names, extra_button=False):
681         """print the dialog form providing sysprep_params"""
682         fields = []
683         for name in names:
684             param = needed[name]
685             default = str(available[name]) if name in available else ""
686             fields.append(("%s: " % param.description, default,
687                            SYSPREP_PARAM_MAXLEN))
688
689         kwargs = {}
690         if extra_button:
691             kwargs['extra_button'] = 1
692             kwargs['extra_label'] = "Advanced"
693
694         txt = "Please provide the following system preparation parameters:"
695         return d.form(txt, height=13, width=WIDTH, form_height=len(fields),
696                       fields=fields, **kwargs)
697
698     def check_params(names, values):
699         """check if the provided sysprep parameters have leagal values"""
700         for i in range(len(names)):
701             param = needed[names[i]]
702             try:
703                 normalized = param.type(values[i])
704                 if param.validate(normalized):
705                     image.os.sysprep_params[names[i]] = normalized
706                     continue
707             except ValueError:
708                 pass
709
710             d.msgbox("Invalid value for parameter: `%s'" % names[i],
711                      width=SMALL_WIDTH)
712             return False
713         return True
714
715     simple_names = [k for k, v in needed.items() if v.default is None]
716     advanced_names = [k for k, v in needed.items() if v.default is not None]
717
718     while 1:
719         code, output = print_form(simple_names, extra_button=True)
720
721         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
722             return False
723         if code == d.DIALOG_EXTRA:
724             while 1:
725                 code, output = print_form(advanced_names)
726                 if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
727                     break
728                 if check_params(advanced_names, output):
729                     break
730             continue
731
732         if check_params(simple_names, output):
733             break
734
735     return True
736
737
738 def sysprep(session):
739     """Perform various system preperation tasks on the image"""
740     d = session['dialog']
741     image = session['image']
742
743     # Is the image already shrinked?
744     if 'shrinked' in session and session['shrinked']:
745         msg = "It seems you have shrinked the image. Running system " \
746               "preparation tasks on a shrinked image is dangerous."
747
748         if d.yesno("%s\n\nDo you really want to continue?" % msg,
749                    width=SMALL_WIDTH, defaultno=1):
750             return
751
752     wrapper = textwrap.TextWrapper(width=WIDTH - 5)
753
754     syspreps = image.os.list_syspreps()
755
756     if len(syspreps) == 0:
757         d.msgbox("No system preparation task available to run!",
758                  title="System Preperation", width=SMALL_WIDTH)
759         return
760
761     while 1:
762         choices = []
763         index = 0
764
765         help_title = "System Preperation Tasks"
766         sysprep_help = "%s\n%s\n\n" % (help_title, '=' * len(help_title))
767
768         for sysprep in syspreps:
769             name, descr = image.os.sysprep_info(sysprep)
770             display_name = name.replace('-', ' ').capitalize()
771             sysprep_help += "%s\n" % display_name
772             sysprep_help += "%s\n" % ('-' * len(display_name))
773             sysprep_help += "%s\n\n" % wrapper.fill(" ".join(descr.split()))
774             enabled = 1 if sysprep.enabled else 0
775             choices.append((str(index + 1), display_name, enabled))
776             index += 1
777
778         (code, tags) = d.checklist(
779             "Please choose which system preparation tasks you would like to "
780             "run on the image. Press <Help> to see details about the system "
781             "preparation tasks.", title="Run system preparation tasks",
782             choices=choices, width=70, ok_label="Run", help_button=1)
783         tags = map(lambda x: x.strip('"'), tags)  # Needed for OpenSUSE
784
785         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
786             return False
787         elif code == d.DIALOG_HELP:
788             d.scrollbox(sysprep_help, width=WIDTH)
789         elif code == d.DIALOG_OK:
790             # Enable selected syspreps and disable the rest
791             for i in range(len(syspreps)):
792                 if str(i + 1) in tags:
793                     image.os.enable_sysprep(syspreps[i])
794                 else:
795                     image.os.disable_sysprep(syspreps[i])
796
797             if len([s for s in image.os.list_syspreps() if s.enabled]) == 0:
798                 d.msgbox("No system preperation task is selected!",
799                          title="System Preperation", width=SMALL_WIDTH)
800                 continue
801
802             if not sysprep_params(session):
803                 continue
804
805             infobox = InfoBoxOutput(d, "Image Configuration")
806             try:
807                 image.out.add(infobox)
808                 try:
809                     # The checksum is invalid. We have mounted the image rw
810                     if 'checksum' in session:
811                         del session['checksum']
812
813                     # Monitor the metadata changes during syspreps
814                     with MetadataMonitor(session, image.os.meta):
815                         try:
816                             image.os.do_sysprep()
817                             infobox.finalize()
818                         except FatalError as e:
819                             title = "System Preparation"
820                             d.msgbox("System Preparation failed: %s" % e,
821                                      title=title, width=SMALL_WIDTH)
822                 finally:
823                     image.out.remove(infobox)
824             finally:
825                 infobox.cleanup()
826             break
827     return True
828
829
830 def shrink(session):
831     """Shrink the image"""
832     d = session['dialog']
833     image = session['image']
834
835     shrinked = 'shrinked' in session and session['shrinked']
836
837     if shrinked:
838         d.msgbox("The image is already shrinked!", title="Image Shrinking",
839                  width=SMALL_WIDTH)
840         return True
841
842     msg = "This operation will shrink the last partition of the image to " \
843           "reduce the total image size. If the last partition is a swap " \
844           "partition, then this partition is removed and the partition " \
845           "before that is shrinked. The removed swap partition will be " \
846           "recreated during image deployment."
847
848     if not d.yesno("%s\n\nDo you want to continue?" % msg, width=WIDTH,
849                    height=12, title="Image Shrinking"):
850         with MetadataMonitor(session, image.meta):
851             infobox = InfoBoxOutput(d, "Image Shrinking", height=4)
852             image.out.add(infobox)
853             try:
854                 image.shrink()
855                 infobox.finalize()
856             finally:
857                 image.out.remove(infobox)
858
859         session['shrinked'] = True
860         update_background_title(session)
861     else:
862         return False
863
864     return True
865
866
867 def customization_menu(session):
868     """Show image customization menu"""
869     d = session['dialog']
870
871     choices = [("Sysprep", "Run various image preparation tasks"),
872                ("Shrink", "Shrink image"),
873                ("View/Modify", "View/Modify image properties"),
874                ("Delete", "Delete image properties"),
875                ("Exclude", "Exclude various deployment tasks from running")]
876
877     default_item = 0
878
879     actions = {"Sysprep": sysprep,
880                "Shrink": shrink,
881                "View/Modify": modify_properties,
882                "Delete": delete_properties,
883                "Exclude": exclude_tasks}
884     while 1:
885         (code, choice) = d.menu(
886             text="Choose one of the following or press <Back> to exit.",
887             width=WIDTH, choices=choices, cancel="Back", height=13,
888             menu_height=len(choices), default_item=choices[default_item][0],
889             title="Image Customization Menu")
890
891         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
892             break
893         elif choice in actions:
894             default_item = [entry[0] for entry in choices].index(choice)
895             if actions[choice](session):
896                 default_item = (default_item + 1) % len(choices)
897
898
899 def main_menu(session):
900     """Show the main menu of the program"""
901     d = session['dialog']
902
903     update_background_title(session)
904
905     choices = [("Customize", "Customize image & cloud deployment options"),
906                ("Register", "Register image to a cloud"),
907                ("Extract", "Dump image to local file system"),
908                ("Reset", "Reset everything and start over again"),
909                ("Help", "Get help for using snf-image-creator")]
910
911     default_item = "Customize"
912
913     actions = {"Customize": customization_menu, "Register": kamaki_menu,
914                "Extract": extract_image}
915     while 1:
916         (code, choice) = d.menu(
917             text="Choose one of the following or press <Exit> to exit.",
918             width=WIDTH, choices=choices, cancel="Exit", height=13,
919             default_item=default_item, menu_height=len(choices),
920             title="Image Creator for synnefo (snf-image-creator version %s)" %
921                   version)
922
923         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
924             if confirm_exit(d):
925                 break
926         elif choice == "Reset":
927             if confirm_reset(d):
928                 d.infobox("Resetting snf-image-creator. Please wait ...",
929                           width=SMALL_WIDTH)
930                 raise Reset
931         elif choice == "Help":
932             d.msgbox("For help, check the online documentation:\n\nhttp://www"
933                      ".synnefo.org/docs/snf-image-creator/latest/",
934                      width=WIDTH, title="Help")
935         elif choice in actions:
936             actions[choice](session)
937
938 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :