Statistics
| Branch: | Tag: | Revision:

root / image_creator / dialog_wizard.py @ 8e58e699

History | View | Annotate | Download (12 kB)

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 "wizard" mode of the dialog-based version of
37
snf-image-creator.
38
"""
39

    
40
import time
41
import StringIO
42
import json
43

    
44
from image_creator.kamaki_wrapper import Kamaki, ClientError
45
from image_creator.util import MD5, FatalError
46
from image_creator.output.cli import OutputWthProgress
47
from image_creator.dialog_util import extract_image, update_background_title, \
48
    add_cloud, edit_cloud
49

    
50
PAGE_WIDTH = 70
51

    
52

    
53
class WizardExit(Exception):
54
    """Exception used to exit the wizard"""
55
    pass
56

    
57

    
58
class WizardInvalidData(Exception):
59
    """Exception triggered when the user provided data are invalid"""
60
    pass
61

    
62

    
63
class Wizard:
64
    """Represents a dialog-based wizard
65

66
    The wizard is a collection of pages that have a "Next" and a "Back" button
67
    on them. The pages are used to collect user data.
68
    """
69

    
70
    def __init__(self, session):
71
        self.session = session
72
        self.pages = []
73
        self.session['wizard'] = {}
74
        self.d = session['dialog']
75

    
76
    def add_page(self, page):
77
        """Add a new page to the wizard"""
78
        self.pages.append(page)
79

    
80
    def run(self):
81
        """Run the wizard"""
82
        idx = 0
83
        while True:
84
            try:
85
                idx += self.pages[idx].run(self.session, idx, len(self.pages))
86
            except WizardExit:
87
                return False
88
            except WizardInvalidData:
89
                continue
90

    
91
            if idx >= len(self.pages):
92
                msg = "All necessary information has been gathered:\n\n"
93
                for page in self.pages:
94
                    msg += " * %s\n" % page.info
95
                msg += "\nContinue with the image creation process?"
96

    
97
                ret = self.d.yesno(
98
                    msg, width=PAGE_WIDTH, height=8 + len(self.pages),
99
                    ok_label="Yes", cancel="Back", extra_button=1,
100
                    extra_label="Quit", title="Confirmation")
101

    
102
                if ret == self.d.DIALOG_CANCEL:
103
                    idx -= 1
104
                elif ret == self.d.DIALOG_EXTRA:
105
                    return False
106
                elif ret == self.d.DIALOG_OK:
107
                    return True
108

    
109
            if idx < 0:
110
                return False
111

    
112

    
113
class WizardPage(object):
114
    """Represents a page in a wizard"""
115
    NEXT = 1
116
    PREV = -1
117

    
118
    def __init__(self, **kargs):
119
        validate = kargs['validate'] if 'validate' in kargs else lambda x: x
120
        setattr(self, "validate", validate)
121

    
122
        display = kargs['display'] if 'display' in kargs else lambda x: x
123
        setattr(self, "display", display)
124

    
125
    def run(self, session, index, total):
126
        """Display this wizard page
127

128
        This function is used by the wizard program when accessing a page.
129
        """
130
        raise NotImplementedError
131

    
132

    
133
class WizardRadioListPage(WizardPage):
134
    """Represent a Radio List in a wizard"""
135
    def __init__(self, name, printable, message, choices, **kargs):
136
        super(WizardRadioListPage, self).__init__(**kargs)
137
        self.name = name
138
        self.printable = printable
139
        self.message = message
140
        self.choices = choices
141
        self.title = kargs['title'] if 'title' in kargs else ''
142
        self.default = kargs['default'] if 'default' in kargs else ""
143

    
144
    def run(self, session, index, total):
145
        d = session['dialog']
146
        w = session['wizard']
147

    
148
        choices = []
149
        for i in range(len(self.choices)):
150
            default = 1 if self.choices[i][0] == self.default else 0
151
            choices.append((self.choices[i][0], self.choices[i][1], default))
152

    
153
        (code, answer) = d.radiolist(
154
            self.message, height=10, width=PAGE_WIDTH, ok_label="Next",
155
            cancel="Back", choices=choices,
156
            title="(%d/%d) %s" % (index + 1, total, self.title))
157

    
158
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
159
            return self.PREV
160

    
161
        w[self.name] = self.validate(answer)
162
        self.default = answer
163
        self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
164

    
165
        return self.NEXT
166

    
167

    
168
class WizardInputPage(WizardPage):
169
    """Represents an input field in a wizard"""
170
    def __init__(self, name, printable, message, **kargs):
171
        super(WizardInputPage, self).__init__(**kargs)
172
        self.name = name
173
        self.printable = printable
174
        self.message = message
175
        self.info = "%s: <none>" % self.printable
176
        self.title = kargs['title'] if 'title' in kargs else ''
177
        self.init = kargs['init'] if 'init' in kargs else ''
178

    
179
    def run(self, session, index, total):
180
        d = session['dialog']
181
        w = session['wizard']
182

    
183
        (code, answer) = d.inputbox(
184
            self.message, init=self.init, width=PAGE_WIDTH, ok_label="Next",
185
            cancel="Back", title="(%d/%d) %s" % (index + 1, total, self.title))
186

    
187
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
188
            return self.PREV
189

    
190
        value = answer.strip()
191
        self.init = value
192
        w[self.name] = self.validate(value)
193
        self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
194

    
195
        return self.NEXT
196

    
197

    
198
def start_wizard(session):
199
    """Run the image creation wizard"""
200

    
201
    d = session['dialog']
202
    clouds = Kamaki.get_clouds()
203
    if not len(clouds):
204
        if not add_cloud(session):
205
            return False
206
    else:
207
        while 1:
208
            choices = []
209
            for (name, cloud) in clouds.items():
210
                descr = cloud['description'] if 'description' in cloud else ''
211
                choices.append((name, descr))
212

    
213
            (code, choice) = d.menu(
214
                "In this menu you can select existing cloud account to use "
215
                " or add new ones. Press <Select> to select an existing "
216
                "account or <Add> to add a new one.", height=18,
217
                width=PAGE_WIDTH, choices=choices, menu_height=10,
218
                ok_label="Select", extra_button=1, extra_label="Add",
219
                title="Clouds")
220

    
221
            if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
222
                return False
223
            elif code == d.DIALOG_OK:  # Select button
224
                account = Kamaki.get_account(choice)
225
                if not account:
226
                    if not d.yesno("Then cloud you have selected is not "
227
                                   "valid! Would you like to edit it?",
228
                                   width=PAGE_WIDTH, defaultno=0):
229
                        edit_cloud(session, choice)
230
                    continue
231
                break
232
            elif code == d.DIALOG_EXTRA:  # Add button
233
                add_cloud(session)
234

    
235
    distro = session['image'].distro
236
    ostype = session['image'].ostype
237
    name = WizardInputPage(
238
        "ImageName", "Image Name", "Please provide a name for the image:",
239
        title="Image Name", init=ostype if distro == "unknown" else distro)
240

    
241
    descr = WizardInputPage(
242
        "ImageDescription", "Image Description",
243
        "Please provide a description for the image:",
244
        title="Image Description", init=session['metadata']['DESCRIPTION'] if
245
        'DESCRIPTION' in session['metadata'] else '')
246

    
247
    registration = WizardRadioListPage(
248
        "ImageRegistration", "Registration Type",
249
        "Please provide a registration type:",
250
        [("Private", "Image is accessible only by this user"),
251
         ("Public", "Everyone can create VMs from this image")],
252
        title="Registration Type", default="Private")
253

    
254
    w = Wizard(session)
255

    
256
    w.add_page(name)
257
    w.add_page(descr)
258
    w.add_page(registration)
259

    
260
    if w.run():
261
        create_image(session, account)
262
    else:
263
        return False
264

    
265
    return True
266

    
267

    
268
def create_image(session, account):
269
    """Create an image using the information collected by the wizard"""
270
    d = session['dialog']
271
    image = session['image']
272
    wizard = session['wizard']
273

    
274
    with_progress = OutputWthProgress(True)
275
    out = image.out
276
    out.add(with_progress)
277
    try:
278
        out.clear()
279

    
280
        #Sysprep
281
        image.os.do_sysprep()
282
        metadata = image.os.meta
283

    
284
        #Shrink
285
        size = image.shrink()
286
        session['shrinked'] = True
287
        update_background_title(session)
288

    
289
        metadata.update(image.meta)
290
        metadata['DESCRIPTION'] = wizard['ImageDescription']
291

    
292
        #MD5
293
        md5 = MD5(out)
294
        session['checksum'] = md5.compute(image.device, size)
295

    
296
        out.output()
297
        try:
298
            out.output("Uploading image to pithos:")
299
            kamaki = Kamaki(account, out)
300

    
301
            name = "%s-%s.diskdump" % (wizard['ImageName'],
302
                                       time.strftime("%Y%m%d%H%M"))
303
            pithos_file = ""
304
            with open(image.device, 'rb') as f:
305
                pithos_file = kamaki.upload(f, size, name,
306
                                            "(1/3)  Calculating block hashes",
307
                                            "(2/3)  Uploading missing blocks")
308

    
309
            out.output("(3/3)  Uploading md5sum file ...", False)
310
            md5sumstr = '%s %s\n' % (session['checksum'], name)
311
            kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr),
312
                          remote_path="%s.%s" % (name, 'md5sum'))
313
            out.success('done')
314
            out.output()
315

    
316
            is_public = True if wizard['ImageRegistration'] == "Public" else \
317
                False
318
            out.output('Registering %s image with cyclades ...' %
319
                       wizard['ImageRegistration'].lower(), False)
320
            result = kamaki.register(wizard['ImageName'], pithos_file,
321
                                     metadata, is_public)
322
            out.success('done')
323
            out.output("Uploading metadata file ...", False)
324
            metastring = unicode(json.dumps(result, ensure_ascii=False))
325
            kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
326
                          remote_path="%s.%s" % (name, 'meta'))
327
            out.success('done')
328

    
329
            if is_public:
330
                out.output("Sharing md5sum file ...", False)
331
                kamaki.share("%s.md5sum" % name)
332
                out.success('done')
333
                out.output("Sharing metadata file ...", False)
334
                kamaki.share("%s.meta" % name)
335
                out.success('done')
336

    
337
            out.output()
338

    
339
        except ClientError as e:
340
            raise FatalError("Pithos client: %d %s" % (e.status, e.message))
341
    finally:
342
        out.remove(with_progress)
343

    
344
    msg = "The %s image was successfully uploaded to Pithos and registered " \
345
          "with Cyclades. Would you like to keep a local copy of the image?" \
346
          % wizard['ImageRegistration'].lower()
347
    if not d.yesno(msg, width=PAGE_WIDTH):
348
        extract_image(session)
349

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