e0e4e04551149956b7d9310fac364e0b1cdcf2f3
[snf-image-creator] / image_creator / dialog_main.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright 2012 GRNET S.A. All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or
7 # without modification, are permitted provided that the following
8 # conditions are met:
9 #
10 #   1. Redistributions of source code must retain the above
11 #      copyright notice, this list of conditions and the following
12 #      disclaimer.
13 #
14 #   2. Redistributions in binary form must reproduce the above
15 #      copyright notice, this list of conditions and the following
16 #      disclaimer in the documentation and/or other materials
17 #      provided with the distribution.
18 #
19 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
20 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
23 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 # POSSIBILITY OF SUCH DAMAGE.
31 #
32 # The views and conclusions contained in the software and
33 # documentation are those of the authors and should not be
34 # interpreted as representing official policies, either expressed
35 # or implied, of GRNET S.A.
36
37 """This module is the entrance point for the dialog-based version of the
38 snf-image-creator program. The main function will create a dialog where the
39 user is asked if he wants to use the program in expert or wizard mode.
40 """
41
42 import dialog
43 import sys
44 import os
45 import stat
46 import textwrap
47 import signal
48 import optparse
49 import types
50
51 from image_creator import __version__ as version
52 from image_creator.util import FatalError
53 from image_creator.output import Output
54 from image_creator.output.cli import SimpleOutput
55 from image_creator.output.dialog import GaugeOutput
56 from image_creator.output.composite import CompositeOutput
57 from image_creator.disk import Disk
58 from image_creator.dialog_wizard import start_wizard
59 from image_creator.dialog_menu import main_menu
60 from image_creator.dialog_util import SMALL_WIDTH, WIDTH, confirm_exit, \
61     Reset, update_background_title
62
63
64 def create_image(d, media, out, tmp):
65     """Create an image out of `media'"""
66     d.setBackgroundTitle('snf-image-creator')
67
68     gauge = GaugeOutput(d, "Initialization", "Initializing...")
69     out.add(gauge)
70     disk = Disk(media, out, tmp)
71
72     def signal_handler(signum, frame):
73         gauge.cleanup()
74         disk.cleanup()
75
76     signal.signal(signal.SIGINT, signal_handler)
77     signal.signal(signal.SIGTERM, signal_handler)
78     try:
79         snapshot = disk.snapshot()
80         image = disk.get_image(snapshot)
81
82         out.output("Collecting image metadata ...")
83         metadata = {}
84         for (key, value) in image.meta.items():
85             metadata[str(key)] = str(value)
86
87         for (key, value) in image.os.meta.items():
88             metadata[str(key)] = str(value)
89
90         out.success("done")
91         gauge.cleanup()
92         out.remove(gauge)
93
94         # Make sure the signal handler does not call gauge.cleanup again
95         def dummy(self):
96             pass
97         gauge.cleanup = type(GaugeOutput.cleanup)(dummy, gauge, GaugeOutput)
98
99         session = {"dialog": d,
100                    "disk": disk,
101                    "image": image,
102                    "metadata": metadata}
103
104         msg = "snf-image-creator detected a %s system on the input media. " \
105               "Would you like to run a wizard to assist you through the " \
106               "image creation process?\n\nChoose <Wizard> to run the wizard," \
107               " <Expert> to run the snf-image-creator in expert mode or " \
108               "press ESC to quit the program." \
109               % (image.ostype if image.ostype == image.distro or
110                  image.distro == "unknown" else "%s (%s)" %
111                  (image.ostype, image.distro))
112
113         update_background_title(session)
114
115         while True:
116             code = d.yesno(msg, width=WIDTH, height=12, yes_label="Wizard",
117                            no_label="Expert")
118             if code == d.DIALOG_OK:
119                 if start_wizard(session):
120                     break
121             elif code == d.DIALOG_CANCEL:
122                 main_menu(session)
123                 break
124
125             if confirm_exit(d):
126                 break
127
128         d.infobox("Thank you for using snf-image-creator. Bye", width=53)
129     finally:
130         disk.cleanup()
131
132     return 0
133
134
135 def select_file(d, media):
136     """Select a media file"""
137     if media == '/':
138         return '/'
139
140     default = os.getcwd() + os.sep
141     while 1:
142         if media is not None:
143             if not os.path.exists(media):
144                 d.msgbox("The file `%s' you choose does not exist." % media,
145                          width=SMALL_WIDTH)
146             else:
147                 mode = os.stat(media).st_mode
148                 if not stat.S_ISDIR(mode):
149                     break
150                 default = media
151
152         (code, media) = d.fselect(default, 10, 60, extra_button=1,
153                                   title="Please select an input media.",
154                                   extra_label="Bundle Host")
155         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
156             if confirm_exit(d, "You canceled the media selection dialog box."):
157                 sys.exit(0)
158             else:
159                 media = None
160                 continue
161         elif code == d.DIALOG_EXTRA:
162             return '/'
163
164     return media
165
166
167 def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[],
168                  **kwargs):
169     """Display a form box.
170
171     fields is in the form: [(label1, item1, item_length1), ...]
172     """
173
174     cmd = ["--form", text, str(height), str(width), str(form_height)]
175
176     label_len = 0
177     for field in fields:
178         if len(field[0]) > label_len:
179             label_len = len(field[0])
180
181     input_len = width - label_len - 2
182
183     line = 1
184     for field in fields:
185         label = field[0]
186         item = field[1]
187         item_len = field[2]
188         cmd.extend((label, str(line), str(1), item, str(line),
189                    str(label_len + 2), str(input_len), str(item_len)))
190         line += 1
191
192     code, output = self._perform(*(cmd,), **kwargs)
193
194     if not output:
195         return (code, [])
196
197     return (code, output.splitlines())
198
199
200 def main():
201
202     d = dialog.Dialog(dialog="dialog")
203
204     # Add extra button in dialog library
205     dialog._common_args_syntax["extra_button"] = \
206         lambda enable: dialog._simple_option("--extra-button", enable)
207
208     dialog._common_args_syntax["extra_label"] = \
209         lambda string: ("--extra-label", string)
210
211     # Allow yes-no label overwriting
212     dialog._common_args_syntax["yes_label"] = \
213         lambda string: ("--yes-label", string)
214
215     dialog._common_args_syntax["no_label"] = \
216         lambda string: ("--no-label", string)
217
218     # Monkey-patch pythondialog to include support for form dialog boxes
219     if not hasattr(dialog, 'form'):
220         d.form = types.MethodType(_dialog_form, d)
221
222     usage = "Usage: %prog [options] [<input_media>]"
223     parser = optparse.OptionParser(version=version, usage=usage)
224     parser.add_option("-l", "--logfile", type="string", dest="logfile",
225                       default=None, help="log all messages to FILE",
226                       metavar="FILE")
227     parser.add_option("--tmpdir", type="string", dest="tmp", default=None,
228                       help="create large temporary image files under DIR",
229                       metavar="DIR")
230
231     options, args = parser.parse_args(sys.argv[1:])
232
233     if len(args) > 1:
234         parser.error("Wrong number of arguments")
235
236     d.setBackgroundTitle('snf-image-creator')
237
238     try:
239         if os.geteuid() != 0:
240             raise FatalError("You must run %s as root" %
241                              parser.get_prog_name())
242
243         if options.tmp is not None and not os.path.isdir(options.tmp):
244             raise FatalError("The directory `%s' specified with --tmpdir is "
245                              "not valid" % options.tmp)
246
247         logfile = None
248         if options.logfile is not None:
249             try:
250                 logfile = open(options.logfile, 'w')
251             except IOError as e:
252                 raise FatalError(
253                     "Unable to open logfile `%s' for writing. Reason: %s" %
254                     (options.logfile, e.strerror))
255
256         media = select_file(d, args[0] if len(args) == 1 else None)
257
258         try:
259             log = SimpleOutput(False, logfile) if logfile is not None \
260                 else Output()
261             while 1:
262                 try:
263                     out = CompositeOutput([log])
264                     out.output("Starting %s v%s ..." %
265                                (parser.get_prog_name(), version))
266                     ret = create_image(d, media, out, options.tmp)
267                     sys.exit(ret)
268                 except Reset:
269                     log.output("Resetting everything ...")
270                     continue
271         finally:
272             if logfile is not None:
273                 logfile.close()
274     except FatalError as e:
275         msg = textwrap.fill(str(e), width=WIDTH)
276         d.infobox(msg, width=WIDTH, title="Fatal Error")
277         sys.exit(1)
278
279 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :