0586327855cbade694d8d0764dc56c24adb30967
[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         if hasattr(image, "unsupported"):
105
106             session['excluded_tasks'] = [-1]
107             session['task_metadata'] = ["EXCLUDE_ALL_TASKS"]
108
109             msg = "The system on the input media is not supported." \
110                 "\n\nReason: %s\n\n" \
111                 "We highly recommend not to create an image out of this, " \
112                 "since the image won't be cleaned up and you will not be " \
113                 "able to configure it during the deployment. Press <YES> if " \
114                 "you still want to continue with the image creation process." \
115                 % image.unsupported
116
117             if not d.yesno(msg, width=WIDTH, defaultno=1, height=12):
118                 main_menu(session)
119
120             d.infobox("Thank you for using snf-image-creator. Bye", width=53)
121             return 0
122
123         msg = "snf-image-creator detected a %s system on the input media. " \
124               "Would you like to run a wizard to assist you through the " \
125               "image creation process?\n\nChoose <Wizard> to run the wizard," \
126               " <Expert> to run the snf-image-creator in expert mode or " \
127               "press ESC to quit the program." \
128               % (image.ostype if image.ostype == image.distro or
129                  image.distro == "unknown" else "%s (%s)" %
130                  (image.ostype, image.distro))
131
132         update_background_title(session)
133
134         while True:
135             code = d.yesno(msg, width=WIDTH, height=12, yes_label="Wizard",
136                            no_label="Expert")
137             if code == d.DIALOG_OK:
138                 if start_wizard(session):
139                     break
140             elif code == d.DIALOG_CANCEL:
141                 main_menu(session)
142                 break
143
144             if confirm_exit(d):
145                 break
146
147         d.infobox("Thank you for using snf-image-creator. Bye", width=53)
148     finally:
149         disk.cleanup()
150
151     return 0
152
153
154 def select_file(d, media):
155     """Select a media file"""
156     if media == '/':
157         return '/'
158
159     default = os.getcwd() + os.sep
160     while 1:
161         if media is not None:
162             if not os.path.exists(media):
163                 d.msgbox("The file `%s' you choose does not exist." % media,
164                          width=SMALL_WIDTH)
165             else:
166                 mode = os.stat(media).st_mode
167                 if not stat.S_ISDIR(mode):
168                     break
169                 default = media
170
171         (code, media) = d.fselect(default, 10, 60, extra_button=1,
172                                   title="Please select an input media.",
173                                   extra_label="Bundle Host")
174         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
175             if confirm_exit(d, "You canceled the media selection dialog box."):
176                 sys.exit(0)
177             else:
178                 media = None
179                 continue
180         elif code == d.DIALOG_EXTRA:
181             return '/'
182
183     return media
184
185
186 def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[],
187                  **kwargs):
188     """Display a form box.
189
190     fields is in the form: [(label1, item1, item_length1), ...]
191     """
192
193     cmd = ["--form", text, str(height), str(width), str(form_height)]
194
195     label_len = 0
196     for field in fields:
197         if len(field[0]) > label_len:
198             label_len = len(field[0])
199
200     input_len = width - label_len - 1
201
202     line = 1
203     for field in fields:
204         label = field[0]
205         item = field[1]
206         item_len = field[2]
207         cmd.extend((label, str(line), str(1), item, str(line),
208                    str(label_len + 1), str(input_len), str(item_len)))
209         line += 1
210
211     code, output = self._perform(*(cmd,), **kwargs)
212
213     if not output:
214         return (code, [])
215
216     return (code, output.splitlines())
217
218
219 def main():
220
221     # In OpenSUSE dialog is buggy under xterm
222     if os.environ['TERM'] == 'xterm':
223         os.environ['TERM'] = 'linux'
224
225     d = dialog.Dialog(dialog="dialog")
226
227     # Add extra button in dialog library
228     dialog._common_args_syntax["extra_button"] = \
229         lambda enable: dialog._simple_option("--extra-button", enable)
230
231     dialog._common_args_syntax["extra_label"] = \
232         lambda string: ("--extra-label", string)
233
234     # Allow yes-no label overwriting
235     dialog._common_args_syntax["yes_label"] = \
236         lambda string: ("--yes-label", string)
237
238     dialog._common_args_syntax["no_label"] = \
239         lambda string: ("--no-label", string)
240
241     # Monkey-patch pythondialog to include support for form dialog boxes
242     if not hasattr(dialog, 'form'):
243         d.form = types.MethodType(_dialog_form, d)
244
245     usage = "Usage: %prog [options] [<input_media>]"
246     parser = optparse.OptionParser(version=version, usage=usage)
247     parser.add_option("-l", "--logfile", type="string", dest="logfile",
248                       default=None, help="log all messages to FILE",
249                       metavar="FILE")
250     parser.add_option("--tmpdir", type="string", dest="tmp", default=None,
251                       help="create large temporary image files under DIR",
252                       metavar="DIR")
253
254     options, args = parser.parse_args(sys.argv[1:])
255
256     if len(args) > 1:
257         parser.error("Wrong number of arguments")
258
259     d.setBackgroundTitle('snf-image-creator')
260
261     try:
262         if os.geteuid() != 0:
263             raise FatalError("You must run %s as root" %
264                              parser.get_prog_name())
265
266         if options.tmp is not None and not os.path.isdir(options.tmp):
267             raise FatalError("The directory `%s' specified with --tmpdir is "
268                              "not valid" % options.tmp)
269
270         logfile = None
271         if options.logfile is not None:
272             try:
273                 logfile = open(options.logfile, 'w')
274             except IOError as e:
275                 raise FatalError(
276                     "Unable to open logfile `%s' for writing. Reason: %s" %
277                     (options.logfile, e.strerror))
278
279         media = select_file(d, args[0] if len(args) == 1 else None)
280
281         try:
282             log = SimpleOutput(False, logfile) if logfile is not None \
283                 else Output()
284             while 1:
285                 try:
286                     out = CompositeOutput([log])
287                     out.output("Starting %s v%s ..." %
288                                (parser.get_prog_name(), version))
289                     ret = create_image(d, media, out, options.tmp)
290                     sys.exit(ret)
291                 except Reset:
292                     log.output("Resetting everything ...")
293                     continue
294         finally:
295             if logfile is not None:
296                 logfile.close()
297     except FatalError as e:
298         msg = textwrap.fill(str(e), width=WIDTH)
299         d.infobox(msg, width=WIDTH, title="Fatal Error")
300         sys.exit(1)
301
302 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :