Statistics
| Branch: | Tag: | Revision:

root / image_creator / dialog_wizard.py @ 769526cb

History | View | Annotate | Download (10.4 kB)

1 09ed3d46 Nikos Skalkotos
#!/usr/bin/env python
2 09ed3d46 Nikos Skalkotos
3 09ed3d46 Nikos Skalkotos
# Copyright 2012 GRNET S.A. All rights reserved.
4 09ed3d46 Nikos Skalkotos
#
5 09ed3d46 Nikos Skalkotos
# Redistribution and use in source and binary forms, with or
6 09ed3d46 Nikos Skalkotos
# without modification, are permitted provided that the following
7 09ed3d46 Nikos Skalkotos
# conditions are met:
8 09ed3d46 Nikos Skalkotos
#
9 09ed3d46 Nikos Skalkotos
#   1. Redistributions of source code must retain the above
10 09ed3d46 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
11 09ed3d46 Nikos Skalkotos
#      disclaimer.
12 09ed3d46 Nikos Skalkotos
#
13 09ed3d46 Nikos Skalkotos
#   2. Redistributions in binary form must reproduce the above
14 09ed3d46 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
15 09ed3d46 Nikos Skalkotos
#      disclaimer in the documentation and/or other materials
16 09ed3d46 Nikos Skalkotos
#      provided with the distribution.
17 09ed3d46 Nikos Skalkotos
#
18 09ed3d46 Nikos Skalkotos
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 09ed3d46 Nikos Skalkotos
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 09ed3d46 Nikos Skalkotos
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 09ed3d46 Nikos Skalkotos
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 09ed3d46 Nikos Skalkotos
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 09ed3d46 Nikos Skalkotos
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 09ed3d46 Nikos Skalkotos
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 09ed3d46 Nikos Skalkotos
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 09ed3d46 Nikos Skalkotos
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 09ed3d46 Nikos Skalkotos
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 09ed3d46 Nikos Skalkotos
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 09ed3d46 Nikos Skalkotos
# POSSIBILITY OF SUCH DAMAGE.
30 09ed3d46 Nikos Skalkotos
#
31 09ed3d46 Nikos Skalkotos
# The views and conclusions contained in the software and
32 09ed3d46 Nikos Skalkotos
# documentation are those of the authors and should not be
33 09ed3d46 Nikos Skalkotos
# interpreted as representing official policies, either expressed
34 09ed3d46 Nikos Skalkotos
# or implied, of GRNET S.A.
35 09ed3d46 Nikos Skalkotos
36 09ed3d46 Nikos Skalkotos
import dialog
37 fbdf1d8f Nikos Skalkotos
import time
38 fbdf1d8f Nikos Skalkotos
import StringIO
39 09ed3d46 Nikos Skalkotos
40 fbdf1d8f Nikos Skalkotos
from image_creator.kamaki_wrapper import Kamaki, ClientError
41 fbdf1d8f Nikos Skalkotos
from image_creator.util import MD5, FatalError
42 fbdf1d8f Nikos Skalkotos
from image_creator.output.cli import OutputWthProgress
43 023e1217 Nikos Skalkotos
from image_creator.dialog_util import extract_image, update_background_title
44 09ed3d46 Nikos Skalkotos
45 3c33e68a Nikos Skalkotos
PAGE_WIDTH = 70
46 09ed3d46 Nikos Skalkotos
47 09ed3d46 Nikos Skalkotos
48 aeb95900 Nikos Skalkotos
class WizardExit(Exception):
49 aeb95900 Nikos Skalkotos
    pass
50 aeb95900 Nikos Skalkotos
51 aeb95900 Nikos Skalkotos
52 31160dc8 Nikos Skalkotos
class WizardInvalidData(Exception):
53 31160dc8 Nikos Skalkotos
    pass
54 31160dc8 Nikos Skalkotos
55 31160dc8 Nikos Skalkotos
56 3c33e68a Nikos Skalkotos
class Wizard:
57 3c33e68a Nikos Skalkotos
    def __init__(self, session):
58 3c33e68a Nikos Skalkotos
        self.session = session
59 3c33e68a Nikos Skalkotos
        self.pages = []
60 3c33e68a Nikos Skalkotos
        self.session['wizard'] = {}
61 baa62724 Nikos Skalkotos
        self.d = session['dialog']
62 3c33e68a Nikos Skalkotos
63 3c33e68a Nikos Skalkotos
    def add_page(self, page):
64 3c33e68a Nikos Skalkotos
        self.pages.append(page)
65 09ed3d46 Nikos Skalkotos
66 09ed3d46 Nikos Skalkotos
    def run(self):
67 3c33e68a Nikos Skalkotos
        idx = 0
68 3c33e68a Nikos Skalkotos
        while True:
69 aeb95900 Nikos Skalkotos
            try:
70 aeb95900 Nikos Skalkotos
                idx += self.pages[idx].run(self.session, idx, len(self.pages))
71 aeb95900 Nikos Skalkotos
            except WizardExit:
72 aeb95900 Nikos Skalkotos
                return False
73 31160dc8 Nikos Skalkotos
            except WizardInvalidData:
74 31160dc8 Nikos Skalkotos
                continue
75 09ed3d46 Nikos Skalkotos
76 3c33e68a Nikos Skalkotos
            if idx >= len(self.pages):
77 baa62724 Nikos Skalkotos
                msg = "All necessary information has been gathered:\n\n"
78 baa62724 Nikos Skalkotos
                for page in self.pages:
79 baa62724 Nikos Skalkotos
                    msg += " * %s\n" % page.info
80 baa62724 Nikos Skalkotos
                msg += "\nConfirm and Proceed."
81 baa62724 Nikos Skalkotos
82 baa62724 Nikos Skalkotos
                ret = self.d.yesno(
83 baa62724 Nikos Skalkotos
                    msg, width=PAGE_WIDTH, height=12, ok_label="Yes",
84 baa62724 Nikos Skalkotos
                    cancel="Back", extra_button=1, extra_label="Quit",
85 baa62724 Nikos Skalkotos
                    title="Confirmation")
86 baa62724 Nikos Skalkotos
87 baa62724 Nikos Skalkotos
                if ret == self.d.DIALOG_CANCEL:
88 baa62724 Nikos Skalkotos
                    idx -= 1
89 baa62724 Nikos Skalkotos
                elif ret == self.d.DIALOG_EXTRA:
90 baa62724 Nikos Skalkotos
                    return False
91 baa62724 Nikos Skalkotos
                elif ret == self.d.DIALOG_OK:
92 baa62724 Nikos Skalkotos
                    return True
93 09ed3d46 Nikos Skalkotos
94 3c33e68a Nikos Skalkotos
            if idx < 0:
95 3c33e68a Nikos Skalkotos
                return False
96 09ed3d46 Nikos Skalkotos
97 09ed3d46 Nikos Skalkotos
98 baa62724 Nikos Skalkotos
class WizardPage(object):
99 3c33e68a Nikos Skalkotos
    NEXT = 1
100 3c33e68a Nikos Skalkotos
    PREV = -1
101 fbdf1d8f Nikos Skalkotos
102 baa62724 Nikos Skalkotos
    def __init__(self, **kargs):
103 baa62724 Nikos Skalkotos
        if 'validate' in kargs:
104 baa62724 Nikos Skalkotos
            validate = kargs['validate']
105 baa62724 Nikos Skalkotos
        else:
106 baa62724 Nikos Skalkotos
            validate = lambda x: x
107 baa62724 Nikos Skalkotos
        setattr(self, "validate", validate)
108 baa62724 Nikos Skalkotos
109 baa62724 Nikos Skalkotos
        if 'display' in kargs:
110 baa62724 Nikos Skalkotos
            display = kargs['display']
111 baa62724 Nikos Skalkotos
        else:
112 baa62724 Nikos Skalkotos
            display = lambda x: x
113 baa62724 Nikos Skalkotos
        setattr(self, "display", display)
114 baa62724 Nikos Skalkotos
115 fbdf1d8f Nikos Skalkotos
    def run(self, session, index, total):
116 fbdf1d8f Nikos Skalkotos
        raise NotImplementedError
117 fbdf1d8f Nikos Skalkotos
118 fbdf1d8f Nikos Skalkotos
119 37ee0098 Nikos Skalkotos
class WizardRadioListPage(WizardPage):
120 37ee0098 Nikos Skalkotos
121 baa62724 Nikos Skalkotos
    def __init__(self, name, printable, message, choices, **kargs):
122 baa62724 Nikos Skalkotos
        super(WizardRadioListPage, self).__init__(**kargs)
123 37ee0098 Nikos Skalkotos
        self.name = name
124 baa62724 Nikos Skalkotos
        self.printable = printable
125 37ee0098 Nikos Skalkotos
        self.message = message
126 37ee0098 Nikos Skalkotos
        self.choices = choices
127 37ee0098 Nikos Skalkotos
        self.title = kargs['title'] if 'title' in kargs else ''
128 baa62724 Nikos Skalkotos
        self.default = kargs['default'] if 'default' in kargs else ""
129 37ee0098 Nikos Skalkotos
130 37ee0098 Nikos Skalkotos
    def run(self, session, index, total):
131 37ee0098 Nikos Skalkotos
        d = session['dialog']
132 37ee0098 Nikos Skalkotos
        w = session['wizard']
133 37ee0098 Nikos Skalkotos
134 37ee0098 Nikos Skalkotos
        choices = []
135 37ee0098 Nikos Skalkotos
        for i in range(len(self.choices)):
136 37d581b8 Nikos Skalkotos
            default = 1 if self.choices[i][0] == self.default else 0
137 37ee0098 Nikos Skalkotos
            choices.append((self.choices[i][0], self.choices[i][1], default))
138 37ee0098 Nikos Skalkotos
139 baa62724 Nikos Skalkotos
        (code, answer) = d.radiolist(
140 baa62724 Nikos Skalkotos
            self.message, height=10, width=PAGE_WIDTH, ok_label="Next",
141 baa62724 Nikos Skalkotos
            cancel="Back", choices=choices,
142 baa62724 Nikos Skalkotos
            title="(%d/%d) %s" % (index + 1, total, self.title))
143 37ee0098 Nikos Skalkotos
144 baa62724 Nikos Skalkotos
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
145 baa62724 Nikos Skalkotos
            return self.PREV
146 37ee0098 Nikos Skalkotos
147 baa62724 Nikos Skalkotos
        w[self.name] = self.validate(answer)
148 baa62724 Nikos Skalkotos
        self.default = answer
149 baa62724 Nikos Skalkotos
        self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
150 37ee0098 Nikos Skalkotos
151 baa62724 Nikos Skalkotos
        return self.NEXT
152 37ee0098 Nikos Skalkotos
153 37ee0098 Nikos Skalkotos
154 fbdf1d8f Nikos Skalkotos
class WizardInputPage(WizardPage):
155 09ed3d46 Nikos Skalkotos
156 baa62724 Nikos Skalkotos
    def __init__(self, name, printable, message, **kargs):
157 baa62724 Nikos Skalkotos
        super(WizardInputPage, self).__init__(**kargs)
158 3c33e68a Nikos Skalkotos
        self.name = name
159 baa62724 Nikos Skalkotos
        self.printable = printable
160 3c33e68a Nikos Skalkotos
        self.message = message
161 3c33e68a Nikos Skalkotos
        self.title = kargs['title'] if 'title' in kargs else ''
162 31160dc8 Nikos Skalkotos
        self.init = kargs['init'] if 'init' in kargs else ''
163 09ed3d46 Nikos Skalkotos
164 3c33e68a Nikos Skalkotos
    def run(self, session, index, total):
165 3c33e68a Nikos Skalkotos
        d = session['dialog']
166 3c33e68a Nikos Skalkotos
        w = session['wizard']
167 09ed3d46 Nikos Skalkotos
168 baa62724 Nikos Skalkotos
        (code, answer) = d.inputbox(
169 baa62724 Nikos Skalkotos
            self.message, init=self.init, width=PAGE_WIDTH, ok_label="Next",
170 baa62724 Nikos Skalkotos
            cancel="Back", title="(%d/%d) %s" % (index + 1, total, self.title))
171 09ed3d46 Nikos Skalkotos
172 baa62724 Nikos Skalkotos
        if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
173 baa62724 Nikos Skalkotos
            return self.PREV
174 09ed3d46 Nikos Skalkotos
175 baa62724 Nikos Skalkotos
        value = answer.strip()
176 baa62724 Nikos Skalkotos
        self.init = value
177 baa62724 Nikos Skalkotos
        w[self.name] = self.validate(value)
178 baa62724 Nikos Skalkotos
        self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
179 09ed3d46 Nikos Skalkotos
180 09ed3d46 Nikos Skalkotos
        return self.NEXT
181 09ed3d46 Nikos Skalkotos
182 09ed3d46 Nikos Skalkotos
183 09ed3d46 Nikos Skalkotos
def wizard(session):
184 8bd0cbb6 Nikos Skalkotos
185 8bd0cbb6 Nikos Skalkotos
    init_token = Kamaki.get_token()
186 8bd0cbb6 Nikos Skalkotos
    if init_token is None:
187 8bd0cbb6 Nikos Skalkotos
        init_token = ""
188 09ed3d46 Nikos Skalkotos
189 baa62724 Nikos Skalkotos
    name = WizardInputPage(
190 baa62724 Nikos Skalkotos
        "ImageName", "Image Name", "Please provide a name for the image:",
191 baa62724 Nikos Skalkotos
        title="Image Name", init=session['device'].distro)
192 baa62724 Nikos Skalkotos
193 37d581b8 Nikos Skalkotos
    descr = WizardInputPage(
194 baa62724 Nikos Skalkotos
        "ImageDescription", "Image Description",
195 baa62724 Nikos Skalkotos
        "Please provide a description for the image:",
196 37d581b8 Nikos Skalkotos
        title="Image Description", init=session['metadata']['DESCRIPTION'] if
197 37d581b8 Nikos Skalkotos
        'DESCRIPTION' in session['metadata'] else '')
198 baa62724 Nikos Skalkotos
199 37d581b8 Nikos Skalkotos
    registration = WizardRadioListPage(
200 baa62724 Nikos Skalkotos
        "ImageRegistration", "Registration Type",
201 baa62724 Nikos Skalkotos
        "Please provide a registration type:",
202 37d581b8 Nikos Skalkotos
        [("Private", "Image is accessible only by this user"),
203 37d581b8 Nikos Skalkotos
         ("Public", "Everyone can create VMs from this image")],
204 37d581b8 Nikos Skalkotos
        title="Registration Type", default="Private")
205 31160dc8 Nikos Skalkotos
206 31160dc8 Nikos Skalkotos
    def validate_account(token):
207 31160dc8 Nikos Skalkotos
        if len(token) == 0:
208 31160dc8 Nikos Skalkotos
            d.msgbox("The token cannot be empty", width=PAGE_WIDTH)
209 31160dc8 Nikos Skalkotos
            raise WizardInvalidData
210 31160dc8 Nikos Skalkotos
211 31160dc8 Nikos Skalkotos
        account = Kamaki.get_account(token)
212 31160dc8 Nikos Skalkotos
        if account is None:
213 31160dc8 Nikos Skalkotos
            session['dialog'].msgbox("The token you provided in not valid!",
214 37d581b8 Nikos Skalkotos
                                     width=PAGE_WIDTH)
215 31160dc8 Nikos Skalkotos
            raise WizardInvalidData
216 31160dc8 Nikos Skalkotos
217 31160dc8 Nikos Skalkotos
        return account
218 31160dc8 Nikos Skalkotos
219 37d581b8 Nikos Skalkotos
    account = WizardInputPage(
220 baa62724 Nikos Skalkotos
        "Account", "Account",
221 baa62724 Nikos Skalkotos
        "Please provide your ~okeanos authentication token:",
222 baa62724 Nikos Skalkotos
        title="~okeanos account", init=init_token, validate=validate_account,
223 baa62724 Nikos Skalkotos
        display=lambda account: account['username'])
224 fbdf1d8f Nikos Skalkotos
225 3c33e68a Nikos Skalkotos
    w = Wizard(session)
226 fbdf1d8f Nikos Skalkotos
227 3c33e68a Nikos Skalkotos
    w.add_page(name)
228 3c33e68a Nikos Skalkotos
    w.add_page(descr)
229 37d581b8 Nikos Skalkotos
    w.add_page(registration)
230 3c33e68a Nikos Skalkotos
    w.add_page(account)
231 fbdf1d8f Nikos Skalkotos
232 fbdf1d8f Nikos Skalkotos
    if w.run():
233 023e1217 Nikos Skalkotos
        create_image(session)
234 fbdf1d8f Nikos Skalkotos
    else:
235 fbdf1d8f Nikos Skalkotos
        return False
236 fbdf1d8f Nikos Skalkotos
237 fbdf1d8f Nikos Skalkotos
    return True
238 fbdf1d8f Nikos Skalkotos
239 fbdf1d8f Nikos Skalkotos
240 023e1217 Nikos Skalkotos
def create_image(session):
241 37ee0098 Nikos Skalkotos
    d = session['dialog']
242 fbdf1d8f Nikos Skalkotos
    disk = session['disk']
243 fbdf1d8f Nikos Skalkotos
    device = session['device']
244 fbdf1d8f Nikos Skalkotos
    snapshot = session['snapshot']
245 fbdf1d8f Nikos Skalkotos
    image_os = session['image_os']
246 fbdf1d8f Nikos Skalkotos
    wizard = session['wizard']
247 fbdf1d8f Nikos Skalkotos
248 8bd0cbb6 Nikos Skalkotos
    # Save Kamaki credentials
249 baa62724 Nikos Skalkotos
    Kamaki.save_token(wizard['Account']['auth_token'])
250 8bd0cbb6 Nikos Skalkotos
251 789a3763 Nikos Skalkotos
    with_progress = OutputWthProgress(True)
252 789a3763 Nikos Skalkotos
    out = disk.out
253 789a3763 Nikos Skalkotos
    out.add(with_progress)
254 789a3763 Nikos Skalkotos
    try:
255 789a3763 Nikos Skalkotos
        out.clear()
256 fbdf1d8f Nikos Skalkotos
257 789a3763 Nikos Skalkotos
        #Sysprep
258 789a3763 Nikos Skalkotos
        device.mount(False)
259 789a3763 Nikos Skalkotos
        image_os.do_sysprep()
260 789a3763 Nikos Skalkotos
        metadata = image_os.meta
261 789a3763 Nikos Skalkotos
        device.umount()
262 1d413d1e Nikos Skalkotos
263 789a3763 Nikos Skalkotos
        #Shrink
264 789a3763 Nikos Skalkotos
        size = device.shrink()
265 3793498a Nikos Skalkotos
        session['shrinked'] = True
266 023e1217 Nikos Skalkotos
        update_background_title(session)
267 fbdf1d8f Nikos Skalkotos
268 789a3763 Nikos Skalkotos
        metadata.update(device.meta)
269 789a3763 Nikos Skalkotos
        metadata['DESCRIPTION'] = wizard['ImageDescription']
270 fbdf1d8f Nikos Skalkotos
271 789a3763 Nikos Skalkotos
        #MD5
272 789a3763 Nikos Skalkotos
        md5 = MD5(out)
273 789a3763 Nikos Skalkotos
        session['checksum'] = md5.compute(snapshot, size)
274 fbdf1d8f Nikos Skalkotos
275 789a3763 Nikos Skalkotos
        #Metadata
276 789a3763 Nikos Skalkotos
        metastring = '\n'.join(
277 789a3763 Nikos Skalkotos
            ['%s=%s' % (key, value) for (key, value) in metadata.items()])
278 789a3763 Nikos Skalkotos
        metastring += '\n'
279 fbdf1d8f Nikos Skalkotos
280 fbdf1d8f Nikos Skalkotos
        out.output()
281 789a3763 Nikos Skalkotos
        try:
282 789a3763 Nikos Skalkotos
            out.output("Uploading image to pithos:")
283 baa62724 Nikos Skalkotos
            kamaki = Kamaki(wizard['Account'], out)
284 789a3763 Nikos Skalkotos
285 789a3763 Nikos Skalkotos
            name = "%s-%s.diskdump" % (wizard['ImageName'],
286 789a3763 Nikos Skalkotos
                                       time.strftime("%Y%m%d%H%M"))
287 789a3763 Nikos Skalkotos
            pithos_file = ""
288 789a3763 Nikos Skalkotos
            with open(snapshot, 'rb') as f:
289 789a3763 Nikos Skalkotos
                pithos_file = kamaki.upload(f, size, name,
290 023e1217 Nikos Skalkotos
                                            "(1/4)  Calculating block hashes",
291 023e1217 Nikos Skalkotos
                                            "(2/4)  Uploading missing blocks")
292 789a3763 Nikos Skalkotos
293 663f5f80 Nikos Skalkotos
            out.output("(3/4)  Uploading metadata file ...", False)
294 789a3763 Nikos Skalkotos
            kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
295 789a3763 Nikos Skalkotos
                          remote_path="%s.%s" % (name, 'meta'))
296 789a3763 Nikos Skalkotos
            out.success('done')
297 663f5f80 Nikos Skalkotos
            out.output("(4/4)  Uploading md5sum file ...", False)
298 789a3763 Nikos Skalkotos
            md5sumstr = '%s %s\n' % (session['checksum'], name)
299 789a3763 Nikos Skalkotos
            kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr),
300 789a3763 Nikos Skalkotos
                          remote_path="%s.%s" % (name, 'md5sum'))
301 789a3763 Nikos Skalkotos
            out.success('done')
302 789a3763 Nikos Skalkotos
            out.output()
303 789a3763 Nikos Skalkotos
304 769526cb Nikos Skalkotos
            is_public = True if wizard['ImageRegistration'] == "Public" else \
305 769526cb Nikos Skalkotos
                False
306 37d581b8 Nikos Skalkotos
            out.output('Registering %s image with ~okeanos ...' %
307 769526cb Nikos Skalkotos
                       wizard['ImageRegistration'].lower(), False)
308 37d581b8 Nikos Skalkotos
            kamaki.register(wizard['ImageName'], pithos_file, metadata,
309 37d581b8 Nikos Skalkotos
                            is_public)
310 789a3763 Nikos Skalkotos
            out.success('done')
311 789a3763 Nikos Skalkotos
            out.output()
312 789a3763 Nikos Skalkotos
313 789a3763 Nikos Skalkotos
        except ClientError as e:
314 789a3763 Nikos Skalkotos
            raise FatalError("Pithos client: %d %s" % (e.status, e.message))
315 789a3763 Nikos Skalkotos
    finally:
316 789a3763 Nikos Skalkotos
        out.remove(with_progress)
317 09ed3d46 Nikos Skalkotos
318 37d581b8 Nikos Skalkotos
    msg = "The %s image was successfully uploaded and registered with " \
319 37d581b8 Nikos Skalkotos
          "~okeanos. Would you like to keep a local copy of the image?" \
320 769526cb Nikos Skalkotos
          % wizard['ImageRegistration'].lower()
321 37ee0098 Nikos Skalkotos
    if not d.yesno(msg, width=PAGE_WIDTH):
322 023e1217 Nikos Skalkotos
        extract_image(session)
323 37ee0098 Nikos Skalkotos
324 09ed3d46 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :