Statistics
| Branch: | Tag: | Revision:

root / image_creator / dialog_main.py @ f9d8c3d9

History | View | Annotate | Download (21 kB)

1
#!/usr/bin/env python
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
import dialog
37
import sys
38
import os
39
import textwrap
40
import signal
41
import StringIO
42

    
43
from image_creator import __version__ as version
44
from image_creator.util import FatalError, MD5
45
from image_creator.output.dialog import InitializationOutput, GaugeOutput
46
from image_creator.disk import Disk
47
from image_creator.os_type import os_cls
48
from image_creator.kamaki_wrapper import Kamaki, ClientError
49

    
50
MSGBOX_WIDTH = 60
51
YESNO_WIDTH = 50
52
MENU_WIDTH = 70
53
INPUTBOX_WIDTH = 70
54

    
55
CONFIGURATION_TASKS = {
56
    "FixPartitionTable":
57
        "Enlarge last partition to use all the available space",
58
    "FilesystemResizeUnmounted":
59
        "Resize file system to use all the available space",
60
    "AddSwap": "Set up the swap partition and add an entry in fstab",
61
    "DeleteSSHKeys": "Remove ssh keys and in some cases recreate them",
62
    "DisableRemoteDesktopConnections":
63
        "Temporary Disable Remote Desktop Connections",
64
    "40SELinuxAutorelabel": "Force the system to relabel at next boot",
65
    "AssignHostname": "Assign Hostname/Computer Name to the instance",
66
    "ChangePassword": "Changes Password for specified users",
67
    "EnforcePersonality": "Inject files to the instance",
68
    "FilesystemResizeMounted":
69
        "Resize filesystem to use all the available space"}
70

    
71

    
72
class Reset(Exception):
73
    pass
74

    
75

    
76
def confirm_exit(d, msg=''):
77
    return not d.yesno("%s Do you want to exit?" % msg, width=YESNO_WIDTH)
78

    
79

    
80
def confirm_reset(d):
81
    return not d.yesno(
82
        "Are you sure you want to reset everything?",
83
        width=YESNO_WIDTH)
84

    
85

    
86
def extract_image(session):
87
    d = session['dialog']
88
    dir = os.getcwd()
89
    while 1:
90
        if dir and dir[-1] != os.sep:
91
            dir = dir + os.sep
92

    
93
        (code, path) = d.fselect(dir, 10, 50, title="Save image as...")
94
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
95
            return False
96

    
97
        if os.path.isdir(path):
98
            dir = path
99
            continue
100

    
101
        if os.path.isdir("%s.meta" % path):
102
            d.msgbox("Can't overwrite directory `%s.meta'" % path,
103
                     width=MSGBOX_WIDTH)
104
            continue
105

    
106
        if os.path.isdir("%s.md5sum" % path):
107
            d.msgbox("Can't overwrite directory `%s.md5sum'" % path,
108
                     width=MSGBOX_WIDTH)
109
            continue
110

    
111
        basedir = os.path.dirname(path)
112
        name = os.path.basename(path)
113
        if not os.path.exists(basedir):
114
            d.msgbox("Directory `%s' does not exist" % basedir,
115
                     width=MSGBOX_WIDTH)
116
            continue
117

    
118
        dir = basedir
119
        if len(name) == 0:
120
            continue
121

    
122
        files = ["%s%s" % (path, ext) for ext in ('', '.meta', '.md5sum')]
123
        overwrite = filter(os.path.exists, files)
124

    
125
        if len(overwrite) > 0:
126
            if d.yesno("The following file(s) exist:\n"
127
                        "%s\nDo you want to overwrite them?" %
128
                        "\n".join(overwrite), width=YESNO_WIDTH):
129
                continue
130

    
131
        out = GaugeOutput(d, "Image Extraction", "Extracting image...")
132
        try:
133
            dev = session['device']
134
            if "checksum" not in session:
135
                size = dev.meta['SIZE']
136
                md5 = MD5(out)
137
                session['checksum'] = md5.compute(session['snapshot'], size)
138

    
139
            # Extract image file
140
            dev.out = out
141
            dev.dump(path)
142

    
143
            # Extract metadata file
144
            out.output("Extracting metadata file...")
145
            metastring = '\n'.join(
146
                ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()])
147
            metastring += '\n'
148
            with open('%s.meta' % path, 'w') as f:
149
                f.write(metastring)
150
            out.success('done')
151

    
152
            # Extract md5sum file
153
            out.output("Extracting md5sum file...")
154
            md5str = "%s %s\n" % (session['checksum'], name)
155
            with open('%s.md5sum' % path, 'w') as f:
156
                f.write(md5str)
157
            out.success("done")
158

    
159
        finally:
160
            out.cleanup()
161
        d.msgbox("Image file `%s' was successfully extracted!" % path,
162
                 width=MSGBOX_WIDTH)
163
        break
164

    
165
    return True
166

    
167

    
168
def upload_image(session):
169
    d = session["dialog"]
170
    size = session['device'].meta['SIZE']
171

    
172
    if "account" not in session:
173
        d.msgbox("You need to provide your ~okeanos login username before you "
174
                 "can upload images to pithos+", width=MSGBOX_WIDTH)
175
        return False
176

    
177
    if "token" not in session:
178
        d.msgbox("You need to provide your ~okeanos account authentication "
179
                 "token before you can upload images to pithos+",
180
                 width=MSGBOX_WIDTH)
181
        return False
182

    
183
    while 1:
184
        init = session["upload"] if "upload" in session else ''
185
        (code, answer) = d.inputbox("Please provide a filename:", init=init,
186
                                    width=INPUTBOX_WIDTH)
187

    
188
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
189
            return False
190

    
191
        filename = answer.strip()
192
        if len(filename) == 0:
193
            d.msgbox("Filename cannot be empty", width=MSGBOX_WIDTH)
194
            continue
195

    
196
        break
197

    
198
    out = GaugeOutput(d, "Image Upload", "Uploading...")
199
    if 'checksum' not in session:
200
        md5 = MD5(out)
201
        session['checksum'] = md5.compute(session['snapshot'], size)
202
    try:
203
        kamaki = Kamaki(session['account'], session['token'], out)
204
        try:
205
            # Upload image file
206
            with open(session['snapshot'], 'rb') as f:
207
                session["upload"] = kamaki.upload(f, size, filename,
208
                                                  "Calculating block hashes",
209
                                                  "Uploading missing blocks")
210
            # Upload metadata file
211
            out.output("Uploading metadata file...")
212
            metastring = '\n'.join(
213
                ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()])
214
            metastring += '\n'
215
            kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
216
                          remote_path="%s.meta" % filename)
217
            out.success("done")
218

    
219
            # Upload md5sum file
220
            out.output("Uploading md5sum file...")
221
            md5str = "%s %s\n" % (session['checksum'], filename)
222
            kamaki.upload(StringIO.StringIO(md5str), size=len(md5str),
223
                          remote_path="%s.md5sum" % filename)
224
            out.success("done")
225

    
226
        except ClientError as e:
227
            d.msgbox("Error in pithos+ client: %s" % e.message,
228
                     title="Pithos+ Client Error", width=MSGBOX_WIDTH)
229
            if 'upload' in session:
230
                del session['upload']
231
            return False
232
    finally:
233
        out.cleanup()
234

    
235
    d.msgbox("Image file `%s' was successfully uploaded to pithos+" % filename,
236
             width=MSGBOX_WIDTH)
237
    return True
238

    
239

    
240
def register_image(session):
241
    d = session["dialog"]
242

    
243
    if "account" not in session:
244
        d.msgbox("You need to provide your ~okeanos login username before you "
245
                 "can register an images to cyclades",
246
                 width=MSGBOX_WIDTH)
247
        return False
248

    
249
    if "token" not in session:
250
        d.msgbox("You need to provide your ~okeanos account authentication "
251
                 "token before you can register an images to cyclades",
252
                 width=MSGBOX_WIDTH)
253
        return False
254

    
255
    if "upload" not in session:
256
        d.msgbox("You need to have an image uploaded to pithos+ before you "
257
                 "can register it to cyclades",
258
                 width=MSGBOX_WIDTH)
259
        return False
260

    
261
    while 1:
262
        (code, answer) = d.inputbox("Please provide a registration name:"
263
                                " be registered:", width=INPUTBOX_WIDTH)
264
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
265
            return False
266

    
267
        name = answer.strip()
268
        if len(name) == 0:
269
            d.msgbox("Registration name cannot be empty", width=MSGBOX_WIDTH)
270
            continue
271
        break
272

    
273
    out = GaugeOutput(d, "Image Registration", "Registrating image...")
274
    try:
275
        out.output("Registring image to cyclades...")
276
        try:
277
            kamaki = Kamaki(session['account'], session['token'], out)
278
            kamaki.register(name, session['upload'], session['metadata'])
279
            out.success('done')
280
        except ClientError as e:
281
            d.msgbox("Error in pithos+ client: %s" % e.message)
282
            return False
283
    finally:
284
        out.cleanup()
285

    
286
    d.msgbox("Image `%s' was successfully registered to cyclades as `%s'" %
287
             (session['upload'], name), width=MSGBOX_WIDTH)
288
    return True
289

    
290

    
291
def kamaki_menu(session):
292
    d = session['dialog']
293
    default_item = "Account"
294
    while 1:
295
        account = session["account"] if "account" in session else "<none>"
296
        token = session["token"] if "token" in session else "<none>"
297
        upload = session["upload"] if "upload" in session else "<none>"
298
        (code, choice) = d.menu(
299
            "Choose one of the following or press <Back> to go back.",
300
            width=MENU_WIDTH,
301
            choices=[("Account", "Change your ~okeanos username: %s" %
302
                      account),
303
                     ("Token", "Change your ~okeanos token: %s" % token),
304
                     ("Upload", "Upload image to pithos+"),
305
                     ("Register", "Register image to cyclades: %s" % upload)],
306
            cancel="Back",
307
            default_item=default_item,
308
            help_button=1,
309
            title="Image Registration Menu")
310

    
311
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
312
            return False
313

    
314
        if choice == "Account":
315
            default_item = "Account"
316
            (code, answer) = d.inputbox(
317
                "Please provide your ~okeanos account e-mail address:",
318
                init=session["account"] if "account" in session else '',
319
                width=70)
320
            if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
321
                continue
322
            if len(answer) == 0 and "account" in session:
323
                    del session["account"]
324
            else:
325
                session["account"] = answer.strip()
326
                default_item = "Token"
327
        elif choice == "Token":
328
            default_item = "Token"
329
            (code, answer) = d.inputbox(
330
                "Please provide your ~okeanos account authetication token:",
331
                init=session["token"] if "token" in session else '',
332
                width=70)
333
            if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
334
                continue
335
            if len(answer) == 0 and "token" in session:
336
                del session["token"]
337
            else:
338
                session["token"] = answer.strip()
339
                default_item = "Upload"
340
        elif choice == "Upload":
341
            if upload_image(session):
342
                default_item = "Register"
343
            else:
344
                default_item = "Upload"
345
        elif choice == "Register":
346
            if register_image(session):
347
                return True
348
            else:
349
                default_item = "Register"
350

    
351

    
352
def add_property(session):
353
    d = session['dialog']
354

    
355
    while 1:
356
        (code, answer) = d.inputbox("Please provide a name for a new image"
357
                                    " property:", width=INPUTBOX_WIDTH)
358
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
359
            return False
360

    
361
        name = answer.strip()
362
        if len(name) == 0:
363
            d.msgbox("A property name cannot be empty", width=MSGBOX_WIDTH)
364
            continue
365

    
366
        break
367

    
368
    while 1:
369
        (code, answer) = d.inputbox("Please provide a value for image "
370
                                   "property %s" % name, width=INPUTBOX_WIDTH)
371
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
372
            return False
373

    
374
        value = answer.strip()
375
        if len(value) == 0:
376
            d.msgbox("Value cannot be empty", width=MSGBOX_WIDTH)
377
            continue
378

    
379
        break
380

    
381
    session['metadata'][name] = value
382

    
383
    return True
384

    
385

    
386
def modify_properties(session):
387
    d = session['dialog']
388

    
389
    while 1:
390
        choices = []
391
        for (key, val) in session['metadata'].items():
392
            choices.append((str(key), str(val)))
393

    
394
        (code, choice) = d.menu(
395
            "In this menu you can edit existing image properties or add new "
396
            "ones. Be carefull! Most properties have special meaning and "
397
            "alter the image deployment behaviour. Press <HELP> to see more "
398
            "information about image properties. Press <BACK> when done.",
399
            height=18,
400
            width=MENU_WIDTH,
401
            choices=choices, menu_height=10,
402
            ok_label="EDIT", extra_button=1, extra_label="ADD", cancel="BACK",
403
            help_button=1, help_label="HELP", title="Image Metadata")
404

    
405
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
406
            break
407
        # Edit button
408
        elif code == d.DIALOG_OK:
409
            (code, answer) = d.inputbox("Please provide a new value for "
410
                    "the image property with name `%s':" % choice,
411
                    init=session['metadata'][choice], width=INPUTBOX_WIDTH)
412
            if code not in (d.DIALOG_CANCEL, d.DIALOG_ESC):
413
                value = answer.strip()
414
                if len(value) == 0:
415
                    d.msgbox("Value cannot be empty!")
416
                    continue
417
                else:
418
                    session['metadata'][choice] = value
419
        # ADD button
420
        elif code == d.DIALOG_EXTRA:
421
            add_property(session)
422

    
423

    
424
def delete_properties(session):
425
    d = session['dialog']
426

    
427
    choices = []
428
    for (key, val) in session['metadata'].items():
429
        choices.append((key, "%s" % val, 0))
430

    
431
    (code, to_delete) = d.checklist("Choose which properties to delete:",
432
                                    choices=choices)
433
    count = len(to_delete)
434
    # If the user exits with ESC or CANCEL, the returned tag list is empty.
435
    for i in to_delete:
436
        del session['metadata'][i]
437

    
438
    if count > 0:
439
        d.msgbox("%d image properties were deleted.", width=MSGBOX_WIDTH)
440

    
441

    
442
def exclude_task(session):
443
    d = session['dialog']
444

    
445
    choices = []
446
    for (key, val) in session['metadata'].items():
447
        choices.append((key, "%s" % val, 0))
448

    
449
    (code, to_delete) = d.checklist("Choose which properties to delete:",
450
                                    choices=choices)
451
    count = len(to_delete)
452
    # If the user exits with ESC or CANCEL, the returned tag list is empty.
453
    for i in to_delete:
454
        del session['metadata'][i]
455

    
456
    if count > 0:
457
        d.msgbox("%d image properties were deleted.", width=MSGBOX_WIDTH)
458

    
459

    
460
def deploy_menu(session):
461
    d = session['dialog']
462

    
463
    default_item = "View/Modify"
464
    actions = {"View/Modify": modify_properties, "Delete": delete_properties}
465
    while 1:
466
        (code, choice) = d.menu(
467
            "Choose one of the following or press <Back> to exit.",
468
            width=MENU_WIDTH,
469
            choices=[("View/Modify", "View/Modify image properties"),
470
                     ("Delete", "Delete image properties"),
471
                     ("Exclude", "Exclude configuration tasks from running")],
472
        cancel="Back",
473
        default_item=default_item,
474
        title="Image Deployment Menu")
475

    
476
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
477
            break
478
        elif choice in actions:
479
            default_item = choice
480
            actions[choice](session)
481

    
482

    
483
def main_menu(session):
484
    d = session['dialog']
485
    dev = session['device']
486
    d.setBackgroundTitle("OS: %s, Distro: %s" % (dev.ostype, dev.distro))
487
    actions = {"Deploy": deploy_menu,
488
               "Register": kamaki_menu,
489
               "Extract": extract_image}
490
    default_item = "Customize"
491

    
492
    while 1:
493
        (code, choice) = d.menu(
494
            "Choose one of the following or press <Exit> to exit.",
495
            width=MENU_WIDTH,
496
            choices=[("Customize", "Run various image customization tasks"),
497
                     ("Deploy", "Configure ~okeanos image deployment options"),
498
                     ("Register", "Register image to ~okeanos"),
499
                     ("Extract", "Dump image to local file system"),
500
                     ("Reset", "Reset everything and start over again"),
501
                     ("Help", "Get help for using snf-image-creator")],
502
            cancel="Exit",
503
            default_item=default_item,
504
            title="Image Creator for ~okeanos (snf-image-creator version %s)" %
505
                  version)
506

    
507
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
508
            if confirm_exit(d):
509
                break
510
            else:
511
                continue
512

    
513
        if choice == "Reset":
514
            if confirm_reset(d):
515
                d.infobox("Resetting snf-image-creator. Please wait...")
516
                raise Reset
517
            else:
518
                continue
519
        elif choice in actions:
520
            actions[choice](session)
521

    
522

    
523
def select_file(d, media):
524
    root = os.sep
525
    while 1:
526
        if media is not None:
527
            if not os.path.exists(media):
528
                d.msgbox("The file you choose does not exist",
529
                         width=MSGBOX_WIDTH)
530
            else:
531
                break
532

    
533
        (code, media) = d.fselect(root, 10, 50,
534
                                 title="Please select input media")
535
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
536
            if confirm_exit(d, "You canceled the media selection dialog box."):
537
                sys.exit(0)
538
            else:
539
                media = None
540
                continue
541

    
542
    return media
543

    
544

    
545
def collect_metadata(dev, out):
546

    
547
    out.output("Collecting image metadata...")
548
    metadata = dev.meta
549
    dev.mount(readonly=True)
550
    cls = os_cls(dev.distro, dev.ostype)
551
    image_os = cls(dev.root, dev.g, out)
552
    dev.umount()
553
    metadata.update(image_os.meta)
554
    out.success("done")
555

    
556
    return metadata
557

    
558

    
559
def image_creator(d):
560
    basename = os.path.basename(sys.argv[0])
561
    usage = "Usage: %s [input_media]" % basename
562
    if len(sys.argv) > 2:
563
        sys.stderr.write("%s\n" % usage)
564
        return 1
565

    
566
    if os.geteuid() != 0:
567
        raise FatalError("You must run %s as root" % basename)
568

    
569
    media = select_file(d, sys.argv[1] if len(sys.argv) == 2 else None)
570

    
571
    out = InitializationOutput(d)
572
    disk = Disk(media, out)
573

    
574
    def signal_handler(signum, fram):
575
        out.cleanup()
576
        disk.cleanup()
577

    
578
    signal.signal(signal.SIGINT, signal_handler)
579
    try:
580
        snapshot = disk.snapshot()
581
        dev = disk.get_device(snapshot)
582

    
583
        metadata = collect_metadata(dev, out)
584
        out.cleanup()
585

    
586
        # Make sure the signal handler does not call out.cleanup again
587
        def dummy(self):
588
            pass
589
        instancemethod = type(InitializationOutput.cleanup)
590
        out.cleanup = instancemethod(dummy, out, InitializationOutput)
591

    
592
        session = {"dialog": d,
593
                   "disk": disk,
594
                   "snapshot": snapshot,
595
                   "device": dev,
596
                   "metadata": metadata}
597

    
598
        main_menu(session)
599
        d.infobox("Thank you for using snf-image-creator. Bye", width=53)
600
    finally:
601
        disk.cleanup()
602

    
603
    return 0
604

    
605

    
606
def main():
607

    
608
    d = dialog.Dialog(dialog="dialog")
609

    
610
    # Add extra button in dialog library
611
    dialog._common_args_syntax["extra_button"] = \
612
        lambda enable: dialog._simple_option("--extra-button", enable)
613

    
614
    dialog._common_args_syntax["extra_label"] = \
615
        lambda string: ("--extra-label", string)
616

    
617
    while 1:
618
        try:
619
            try:
620
                ret = image_creator(d)
621
                sys.exit(ret)
622
            except FatalError as e:
623
                msg = textwrap.fill(str(e), width=70)
624
                d.infobox(msg, width=70, title="Fatal Error")
625
                sys.exit(1)
626
        except Reset:
627
            continue
628

    
629
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :