fee232a21d3a7407b8781f4bcfbf74f1ad386f57
[snf-image-creator] / image_creator / dialog_util.py
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 """Module providing useful functions for the dialog-based version of
37 snf-image-creator.
38 """
39
40 import os
41 import re
42 from image_creator.output.dialog import GaugeOutput
43 from image_creator.util import MD5
44 from image_creator.kamaki_wrapper import Kamaki
45
46 SMALL_WIDTH = 60
47 WIDTH = 70
48
49
50 def update_background_title(session):
51     """Update the backgroud title of the dialog page"""
52     d = session['dialog']
53     disk = session['disk']
54     image = session['image']
55
56     MB = 2 ** 20
57
58     size = (image.size + MB - 1) // MB
59     shrinked = 'shrinked' in session and session['shrinked']
60     postfix = " (shrinked)" if shrinked else ''
61
62     title = "OS: %s, Distro: %s, Size: %dMB%s, Source: %s" % \
63             (image.ostype, image.distro, size, postfix,
64              os.path.abspath(disk.source))
65
66     d.setBackgroundTitle(title)
67
68
69 def confirm_exit(d, msg=''):
70     """Ask the user to confirm when exiting the program"""
71     return not d.yesno("%s Do you want to exit?" % msg, width=SMALL_WIDTH)
72
73
74 def confirm_reset(d):
75     """Ask the user to confirm a reset action"""
76     return not d.yesno("Are you sure you want to reset everything?",
77                        width=SMALL_WIDTH, defaultno=1)
78
79
80 class Reset(Exception):
81     """Exception used to reset the program"""
82     pass
83
84
85 def extract_metadata_string(session):
86     """Convert image metadata to text"""
87     metadata = ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()]
88
89     if 'task_metadata' in session:
90         metadata.extend("%s=yes" % m for m in session['task_metadata'])
91
92     return '\n'.join(metadata) + '\n'
93
94
95 def extract_image(session):
96     """Dump the image to a local file"""
97     d = session['dialog']
98     dir = os.getcwd()
99     while 1:
100         if dir and dir[-1] != os.sep:
101             dir = dir + os.sep
102
103         (code, path) = d.fselect(dir, 10, 50, title="Save image as...")
104         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
105             return False
106
107         if os.path.isdir(path):
108             dir = path
109             continue
110
111         if os.path.isdir("%s.meta" % path):
112             d.msgbox("Can't overwrite directory `%s.meta'" % path,
113                      width=SMALL_WIDTH)
114             continue
115
116         if os.path.isdir("%s.md5sum" % path):
117             d.msgbox("Can't overwrite directory `%s.md5sum'" % path,
118                      width=SMALL_WIDTH)
119             continue
120
121         basedir = os.path.dirname(path)
122         name = os.path.basename(path)
123         if not os.path.exists(basedir):
124             d.msgbox("Directory `%s' does not exist" % basedir,
125                      width=SMALL_WIDTH)
126             continue
127
128         dir = basedir
129         if len(name) == 0:
130             continue
131
132         files = ["%s%s" % (path, ext) for ext in ('', '.meta', '.md5sum')]
133         overwrite = filter(os.path.exists, files)
134
135         if len(overwrite) > 0:
136             if d.yesno("The following file(s) exist:\n"
137                        "%s\nDo you want to overwrite them?" %
138                        "\n".join(overwrite), width=SMALL_WIDTH):
139                 continue
140
141         gauge = GaugeOutput(d, "Image Extraction", "Extracting image...")
142         try:
143             image = session['image']
144             out = image.out
145             out.add(gauge)
146             try:
147                 if "checksum" not in session:
148                     md5 = MD5(out)
149                     session['checksum'] = md5.compute(image.device, image.size)
150
151                 # Extract image file
152                 image.dump(path)
153
154                 # Extract metadata file
155                 out.output("Extracting metadata file ...")
156                 with open('%s.meta' % path, 'w') as f:
157                     f.write(extract_metadata_string(session))
158                 out.success('done')
159
160                 # Extract md5sum file
161                 out.output("Extracting md5sum file ...")
162                 md5str = "%s %s\n" % (session['checksum'], name)
163                 with open('%s.md5sum' % path, 'w') as f:
164                     f.write(md5str)
165                 out.success("done")
166             finally:
167                 out.remove(gauge)
168         finally:
169             gauge.cleanup()
170         d.msgbox("Image file `%s' was successfully extracted!" % path,
171                  width=SMALL_WIDTH)
172         break
173
174     return True
175
176
177 def _check_cloud(session, name, description, url, token):
178     """Checks if the provided info for a cloud are valid"""
179     d = session['dialog']
180     regexp = re.compile('^[a-zA-Z0-9_]+$')
181
182     if not re.match(regexp, name):
183         d.msgbox("Allowed characters for name: [a-zA-Z0-9_]", width=WIDTH)
184         return False
185
186     if len(url) == 0:
187         d.msgbox("Url cannot be empty!", width=WIDTH)
188         return False
189
190     if len(token) == 0:
191         d.msgbox("Token cannot be empty!", width=WIDTH)
192         return False
193
194     if Kamaki.create_account(url, token) is None:
195         d.msgbox("The cloud info you provided is not valid. Please check the "
196                  "Authentication URL and the token values again!", width=WIDTH)
197         return False
198
199     return True
200
201
202 def add_cloud(session):
203     """Add a new cloud account"""
204
205     d = session['dialog']
206
207     name = ""
208     description = ""
209     url = ""
210     token = ""
211
212     while 1:
213         fields = [
214             ("Name:", name, 60),
215             ("Description (optional): ", description, 80),
216             ("Authentication URL: ", url, 200),
217             ("Token:", token, 100)]
218
219         (code, output) = d.form("Add a new cloud account:", height=13,
220                                 width=WIDTH, form_height=4, fields=fields)
221
222         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
223             return False
224
225         name, description, url, token = output
226
227         name = name.strip()
228         description = description.strip()
229         url = url.strip()
230         token = token.strip()
231
232         if _check_cloud(session, name, description, url, token):
233             if name in Kamaki.get_clouds().keys():
234                 d.msgbox("A cloud with name `%s' already exists. If you want "
235                          "to edit the existing cloud account, use the edit "
236                          "menu." % name, width=WIDTH)
237             else:
238                 Kamaki.save_cloud(name, url, token, description)
239                 break
240
241         continue
242
243     return True
244
245
246 def edit_cloud(session, name):
247     """Edit a cloud account"""
248
249     info = Kamaki.get_cloud_by_name(name)
250
251     assert info, "Cloud: `%s' does not exist" % name
252     assert 'url' in info, "Cloud: `%s' does not have a url attr" % name
253     assert 'token' in info, "Cloud: `%s' does not have a token attr" % name
254
255     description = info['description'] if 'description' in info else ""
256     url = info['url']
257     token = info['token']
258
259     d = session['dialog']
260
261     while 1:
262         fields = [
263             ("Description (optional): ", description, 80),
264             ("Authentication URL: ", url, 200),
265             ("Token:", token, 100)]
266
267         (code, output) = d.form("Edit cloud account: `%s'" % name, height=13,
268                                 width=WIDTH, form_height=3, fields=fields)
269
270         if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
271             return False
272
273         description, url, token = output
274
275         description = description.strip()
276         url = url.strip()
277         token = token.strip()
278
279         if _check_cloud(session, name, description, url, token):
280             Kamaki.save_cloud(name, url, token, description)
281             break
282
283         continue
284
285     return True
286
287 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :