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