Add logging service in snf-image-creator-dialog
[snf-image-creator] / image_creator / dialog_wizard.py
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 time
38 import StringIO
39
40 from image_creator.kamaki_wrapper import Kamaki, ClientError
41 from image_creator.util import MD5, FatalError
42 from image_creator.output.cli import OutputWthProgress
43
44 PAGE_WIDTH = 70
45
46
47 class WizardExit(Exception):
48     pass
49
50
51 class Wizard:
52     def __init__(self, session):
53         self.session = session
54         self.pages = []
55         self.session['wizard'] = {}
56
57     def add_page(self, page):
58         self.pages.append(page)
59
60     def run(self):
61         idx = 0
62         while True:
63             try:
64                 idx += self.pages[idx].run(self.session, idx, len(self.pages))
65             except WizardExit:
66                 return False
67
68             if idx >= len(self.pages):
69                 break
70
71             if idx < 0:
72                 return False
73         return True
74
75
76 class WizardPage:
77     NEXT = 1
78     PREV = -1
79
80     def run(self, session, index, total):
81         raise NotImplementedError
82
83
84 class WizardRadioListPage(WizardPage):
85
86     def __init__(self, name, message, choices, **kargs):
87         self.name = name
88         self.message = message
89         self.choices = choices
90         self.title = kargs['title'] if 'title' in kargs else ''
91         self.default = kargs['default'] if 'default' in kargs else 0
92
93     def run(self, session, index, total):
94         d = session['dialog']
95         w = session['wizard']
96
97         choices = []
98         for i in range(len(self.choices)):
99             default = 1 if i == self.default else 0
100             choices.append((self.choices[i][0], self.choices[i][1], default))
101
102         while True:
103             (code, answer) = d.radiolist(self.message, width=PAGE_WIDTH,
104                 ok_label="Next", cancel="Back", choices=choices,
105                 title="(%d/%d) %s" % (index + 1, total, self.title))
106
107             if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
108                 return self.PREV
109
110             for i in range(len(choices)):
111                 if self.choices[i] == answer:
112                     self.default = i
113                     w[name] = i
114                     break
115
116             return self.NEXT
117
118
119 class WizardInputPage(WizardPage):
120
121     def __init__(self, name, message, **kargs):
122         self.name = name
123         self.message = message
124         self.title = kargs['title'] if 'title' in kargs else ''
125         self.init_value = kargs['init'] if 'init' in kargs else ''
126         self.allow_empty = kargs['empty'] if 'empty' in kargs else False
127
128     def run(self, session, index, total):
129         d = session['dialog']
130         w = session['wizard']
131
132         init = w[self.name] if self.name in w else self.init_value
133         while True:
134             (code, answer) = d.inputbox(self.message, init=init,
135                 width=PAGE_WIDTH, ok_label="Next", cancel="Back",
136                 title="(%d/%d) %s" % (index + 1, total, self.title))
137
138             if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
139                 return self.PREV
140
141             value = answer.strip()
142             if len(value) == 0 and self.allow_empty is False:
143                 d.msgbox("The value cannot be empty!", width=PAGE_WIDTH)
144                 continue
145             w[self.name] = value
146             break
147
148         return self.NEXT
149
150
151 class WizardYesNoPage(WizardPage):
152
153     def __init__(self, message, **kargs):
154         self.message = message
155         self.title = kargs['title'] if 'title' in kargs else ''
156
157     def run(self, session, index, total):
158         d = session['dialog']
159
160         while True:
161             ret = d.yesno(self.message, width=PAGE_WIDTH, ok_label="Yes",
162                     cancel="Back", extra_button=1, extra_label="Quit",
163                     title="(%d/%d) %s" % (index + 1, total, self.title))
164
165             if ret == d.DIALOG_CANCEL:
166                 return self.PREV
167             elif ret == d.DIALOG_EXTRA:
168                 raise WizardExit
169             elif ret == d.DIALOG_OK:
170                 return self.NEXT
171
172
173 def wizard(session):
174
175     name = WizardInputPage("ImageName", "Please provide a name for the image:",
176                       title="Image Name", init=session['device'].distro)
177     descr = WizardInputPage("ImageDescription",
178         "Please provide a description for the image:",
179         title="Image Description", empty=True,
180         init=session['metadata']['DESCRIPTION'] if 'DESCRIPTION' in
181         session['metadata'] else '')
182     account = WizardInputPage("account",
183         "Please provide your ~okeanos account e-mail:",
184         title="~okeanos account information", init=Kamaki.get_account())
185     token = WizardInputPage("token",
186         "Please provide your ~okeanos account token:",
187         title="~okeanos account token", init=Kamaki.get_token())
188
189     msg = "Do you wish to continue with the image extraction process?"
190     proceed = WizardYesNoPage(msg, title="Confirmation")
191
192     w = Wizard(session)
193
194     w.add_page(name)
195     w.add_page(descr)
196     w.add_page(account)
197     w.add_page(token)
198     w.add_page(proceed)
199
200     if w.run():
201         extract_image(session)
202     else:
203         return False
204
205     return True
206
207
208 def extract_image(session):
209     d = session['dialog']
210     disk = session['disk']
211     device = session['device']
212     snapshot = session['snapshot']
213     image_os = session['image_os']
214     wizard = session['wizard']
215
216     with_progress = OutputWthProgress(True)
217     out = disk.out
218     out.add(with_progress)
219     try:
220         out.clear()
221
222         #Sysprep
223         device.mount(False)
224         image_os.do_sysprep()
225         metadata = image_os.meta
226         device.umount()
227
228         #Shrink
229         size = device.shrink()
230
231         metadata.update(device.meta)
232         metadata['DESCRIPTION'] = wizard['ImageDescription']
233
234         #MD5
235         md5 = MD5(out)
236         session['checksum'] = md5.compute(snapshot, size)
237
238         #Metadata
239         metastring = '\n'.join(
240             ['%s=%s' % (key, value) for (key, value) in metadata.items()])
241         metastring += '\n'
242
243         out.output()
244         try:
245             out.output("Uploading image to pithos:")
246             kamaki = Kamaki(wizard['account'], wizard['token'], out)
247
248             name = "%s-%s.diskdump" % (wizard['ImageName'],
249                                        time.strftime("%Y%m%d%H%M"))
250             pithos_file = ""
251             with open(snapshot, 'rb') as f:
252                 pithos_file = kamaki.upload(f, size, name,
253                                              "(1/4)  Calculating block hashes",
254                                              "(2/4)  Uploading missing blocks")
255
256             out.output("(3/4)  Uploading metadata file...", False)
257             kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
258                           remote_path="%s.%s" % (name, 'meta'))
259             out.success('done')
260             out.output("(4/4)  Uploading md5sum file...", False)
261             md5sumstr = '%s %s\n' % (session['checksum'], name)
262             kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr),
263                           remote_path="%s.%s" % (name, 'md5sum'))
264             out.success('done')
265             out.output()
266
267             out.output('Registring image to ~okeanos...', False)
268             kamaki.register(wizard['ImageName'], pithos_file, metadata)
269             out.success('done')
270             out.output()
271
272         except ClientError as e:
273             raise FatalError("Pithos client: %d %s" % (e.status, e.message))
274     finally:
275         out.remove(with_progress)
276
277     msg = "The image was successfully uploaded and registered to " \
278           "~okeanos. Would you like to keep a local copy of the image?"
279     if not d.yesno(msg, width=PAGE_WIDTH):
280         getattr(__import__("image_creator.dialog_main",
281                 fromlist=['image_creator']), "extract_image")(session)
282
283
284 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :