Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 2c4a641b

History | View | Annotate | Download (32.4 kB)

1
#!/usr/bin/env python
2

    
3
"""
4
Synnefo ci utils module
5
"""
6

    
7
import os
8
import re
9
import sys
10
import time
11
import logging
12
import fabric.api as fabric
13
import subprocess
14
import tempfile
15
from ConfigParser import ConfigParser, DuplicateSectionError
16

    
17
from kamaki.cli import config as kamaki_config
18
from kamaki.clients.astakos import AstakosClient
19
from kamaki.clients.cyclades import CycladesClient
20
from kamaki.clients.image import ImageClient
21
from kamaki.clients.compute import ComputeClient
22
import filelocker
23

    
24
DEFAULT_CONFIG_FILE = "ci_squeeze.conf"
25
# UUID of owner of system images
26
DEFAULT_SYSTEM_IMAGES_UUID = [
27
    "25ecced9-bf53-4145-91ee-cf47377e9fb2",  # production (okeanos.grnet.gr)
28
    "04cbe33f-29b7-4ef1-94fb-015929e5fc06",  # testing (okeanos.io)
29
]
30

    
31

    
32
def _run(cmd, verbose):
33
    """Run fabric with verbose level"""
34
    if verbose:
35
        args = ('running',)
36
    else:
37
        args = ('running', 'stdout',)
38
    with fabric.hide(*args):  # Used * or ** magic. pylint: disable-msg=W0142
39
        return fabric.run(cmd)
40

    
41

    
42
def _put(local, remote):
43
    """Run fabric put command without output"""
44
    with fabric.quiet():
45
        fabric.put(local, remote)
46

    
47

    
48
def _red(msg):
49
    """Red color"""
50
    #return "\x1b[31m" + str(msg) + "\x1b[0m"
51
    return str(msg)
52

    
53

    
54
def _yellow(msg):
55
    """Yellow color"""
56
    #return "\x1b[33m" + str(msg) + "\x1b[0m"
57
    return str(msg)
58

    
59

    
60
def _green(msg):
61
    """Green color"""
62
    #return "\x1b[32m" + str(msg) + "\x1b[0m"
63
    return str(msg)
64

    
65

    
66
def _check_fabric(fun):
67
    """Check if fabric env has been set"""
68
    def wrapper(self, *args, **kwargs):
69
        """wrapper function"""
70
        if not self.fabric_installed:
71
            self.setup_fabric()
72
            self.fabric_installed = True
73
        return fun(self, *args, **kwargs)
74
    return wrapper
75

    
76

    
77
def _check_kamaki(fun):
78
    """Check if kamaki has been initialized"""
79
    def wrapper(self, *args, **kwargs):
80
        """wrapper function"""
81
        if not self.kamaki_installed:
82
            self.setup_kamaki()
83
            self.kamaki_installed = True
84
        return fun(self, *args, **kwargs)
85
    return wrapper
86

    
87

    
88
class _MyFormatter(logging.Formatter):
89
    """Logging Formatter"""
90
    def format(self, record):
91
        format_orig = self._fmt
92
        if record.levelno == logging.DEBUG:
93
            self._fmt = "  %(msg)s"
94
        elif record.levelno == logging.INFO:
95
            self._fmt = "%(msg)s"
96
        elif record.levelno == logging.WARNING:
97
            self._fmt = _yellow("[W] %(msg)s")
98
        elif record.levelno == logging.ERROR:
99
            self._fmt = _red("[E] %(msg)s")
100
        result = logging.Formatter.format(self, record)
101
        self._fmt = format_orig
102
        return result
103

    
104

    
105
# Too few public methods. pylint: disable-msg=R0903
106
class _InfoFilter(logging.Filter):
107
    """Logging Filter that allows DEBUG and INFO messages only"""
108
    def filter(self, rec):
109
        """The filter"""
110
        return rec.levelno in (logging.DEBUG, logging.INFO)
111

    
112

    
113
# Too many instance attributes. pylint: disable-msg=R0902
114
class SynnefoCI(object):
115
    """SynnefoCI python class"""
116

    
117
    def __init__(self, config_file=None, build_id=None, cloud=None):
118
        """ Initialize SynnefoCI python class
119

120
        Setup logger, local_dir, config and kamaki
121
        """
122
        # Setup logger
123
        self.logger = logging.getLogger('synnefo-ci')
124
        self.logger.setLevel(logging.DEBUG)
125

    
126
        handler1 = logging.StreamHandler(sys.stdout)
127
        handler1.setLevel(logging.DEBUG)
128
        handler1.addFilter(_InfoFilter())
129
        handler1.setFormatter(_MyFormatter())
130
        handler2 = logging.StreamHandler(sys.stderr)
131
        handler2.setLevel(logging.WARNING)
132
        handler2.setFormatter(_MyFormatter())
133

    
134
        self.logger.addHandler(handler1)
135
        self.logger.addHandler(handler2)
136

    
137
        # Get our local dir
138
        self.ci_dir = os.path.dirname(os.path.abspath(__file__))
139
        self.repo_dir = os.path.dirname(self.ci_dir)
140

    
141
        # Read config file
142
        if config_file is None:
143
            config_file = os.path.join(self.ci_dir, DEFAULT_CONFIG_FILE)
144
        config_file = os.path.abspath(config_file)
145
        self.config = ConfigParser()
146
        self.config.optionxform = str
147
        self.config.read(config_file)
148

    
149
        # Read temporary_config file
150
        self.temp_config_file = \
151
            os.path.expanduser(self.config.get('Global', 'temporary_config'))
152
        self.temp_config = ConfigParser()
153
        self.temp_config.optionxform = str
154
        self.temp_config.read(self.temp_config_file)
155
        self.build_id = build_id
156
        self.logger.info("Will use \"%s\" as build id" % _green(self.build_id))
157

    
158
        # Set kamaki cloud
159
        if cloud is not None:
160
            self.kamaki_cloud = cloud
161
        elif self.config.has_option("Deployment", "kamaki_cloud"):
162
            kamaki_cloud = self.config.get("Deployment", "kamaki_cloud")
163
            if kamaki_cloud == "":
164
                self.kamaki_cloud = None
165
        else:
166
            self.kamaki_cloud = None
167

    
168
        # Initialize variables
169
        self.fabric_installed = False
170
        self.kamaki_installed = False
171
        self.cyclades_client = None
172
        self.compute_client = None
173
        self.image_client = None
174
        self.astakos_client = None
175

    
176
    def setup_kamaki(self):
177
        """Initialize kamaki
178

179
        Setup cyclades_client, image_client and compute_client
180
        """
181

    
182
        config = kamaki_config.Config()
183
        if self.kamaki_cloud is None:
184
            self.kamaki_cloud = config.get_global("default_cloud")
185

    
186
        self.logger.info("Setup kamaki client, using cloud '%s'.." %
187
                         self.kamaki_cloud)
188
        auth_url = config.get_cloud(self.kamaki_cloud, "url")
189
        self.logger.debug("Authentication URL is %s" % _green(auth_url))
190
        token = config.get_cloud(self.kamaki_cloud, "token")
191
        #self.logger.debug("Token is %s" % _green(token))
192

    
193
        self.astakos_client = AstakosClient(auth_url, token)
194

    
195
        cyclades_url = \
196
            self.astakos_client.get_service_endpoints('compute')['publicURL']
197
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
198
        self.cyclades_client = CycladesClient(cyclades_url, token)
199
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
200

    
201
        image_url = \
202
            self.astakos_client.get_service_endpoints('image')['publicURL']
203
        self.logger.debug("Images API url is %s" % _green(image_url))
204
        self.image_client = ImageClient(cyclades_url, token)
205
        self.image_client.CONNECTION_RETRY_LIMIT = 2
206

    
207
        compute_url = \
208
            self.astakos_client.get_service_endpoints('compute')['publicURL']
209
        self.logger.debug("Compute API url is %s" % _green(compute_url))
210
        self.compute_client = ComputeClient(compute_url, token)
211
        self.compute_client.CONNECTION_RETRY_LIMIT = 2
212

    
213
    def _wait_transition(self, server_id, current_status, new_status):
214
        """Wait for server to go from current_status to new_status"""
215
        self.logger.debug("Waiting for server to become %s" % new_status)
216
        timeout = self.config.getint('Global', 'build_timeout')
217
        sleep_time = 5
218
        while True:
219
            server = self.cyclades_client.get_server_details(server_id)
220
            if server['status'] == new_status:
221
                return server
222
            elif timeout < 0:
223
                self.logger.error(
224
                    "Waiting for server to become %s timed out" % new_status)
225
                self.destroy_server(False)
226
                sys.exit(-1)
227
            elif server['status'] == current_status:
228
                # Sleep for #n secs and continue
229
                timeout = timeout - sleep_time
230
                time.sleep(sleep_time)
231
            else:
232
                self.logger.error(
233
                    "Server failed with status %s" % server['status'])
234
                self.destroy_server(False)
235
                sys.exit(-1)
236

    
237
    @_check_kamaki
238
    def destroy_server(self, wait=True):
239
        """Destroy slave server"""
240
        server_id = int(self.read_temp_config('server_id'))
241
        self.logger.info("Destoying server with id %s " % server_id)
242
        self.cyclades_client.delete_server(server_id)
243
        if wait:
244
            self._wait_transition(server_id, "ACTIVE", "DELETED")
245

    
246
    @_check_kamaki
247
    def create_server(self, image=None, flavor=None, ssh_keys=None):
248
        """Create slave server"""
249
        self.logger.info("Create a new server..")
250

    
251
        # Find a build_id to use
252
        if self.build_id is None:
253
            # If build_id is given use this, else ..
254
            # Find a uniq build_id to use
255
            ids = self.temp_config.sections()
256
            if ids:
257
                max_id = int(max(self.temp_config.sections(), key=int))
258
                self.build_id = max_id + 1
259
            else:
260
                self.build_id = 1
261
        self.logger.debug("New build id \"%s\" was created"
262
                          % _green(self.build_id))
263

    
264
        # Find an image to use
265
        image_id = self._find_image(image)
266
        # Find a flavor to use
267
        flavor_id = self._find_flavor(flavor)
268

    
269
        # Create Server
270
        server_name = self.config.get("Deployment", "server_name")
271
        server = self.cyclades_client.create_server(
272
            "%s(BID: %s)" % (server_name, self.build_id),
273
            flavor_id,
274
            image_id)
275
        server_id = server['id']
276
        self.write_temp_config('server_id', server_id)
277
        self.logger.debug("Server got id %s" % _green(server_id))
278
        server_user = server['metadata']['users']
279
        self.write_temp_config('server_user', server_user)
280
        self.logger.debug("Server's admin user is %s" % _green(server_user))
281
        server_passwd = server['adminPass']
282
        self.write_temp_config('server_passwd', server_passwd)
283

    
284
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
285
        self._get_server_ip_and_port(server)
286
        self._copy_ssh_keys(ssh_keys)
287

    
288
        # Setup Firewall
289
        self.setup_fabric()
290
        self.logger.info("Setup firewall")
291
        accept_ssh_from = self.config.get('Global', 'accept_ssh_from')
292
        if accept_ssh_from != "":
293
            self.logger.debug("Block ssh except from %s" % accept_ssh_from)
294
            cmd = """
295
            local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
296
                cut -d':' -f2 | cut -d' ' -f1)
297
            iptables -A INPUT -s localhost -j ACCEPT
298
            iptables -A INPUT -s $local_ip -j ACCEPT
299
            iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
300
            iptables -A INPUT -p tcp --dport 22 -j DROP
301
            """.format(accept_ssh_from)
302
            _run(cmd, False)
303

    
304
        # Setup apt, download packages
305
        self.logger.debug("Setup apt. Install x2goserver and firefox")
306
        cmd = """
307
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
308
        apt-get update
309
        apt-get install curl --yes --force-yes
310
        echo -e "\n\n{0}" >> /etc/apt/sources.list
311
        # Synnefo repo's key
312
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
313

314
        # X2GO Key
315
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
316
        apt-get install x2go-keyring --yes --force-yes
317
        apt-get update
318
        apt-get install x2goserver x2goserver-xsession \
319
                iceweasel --yes --force-yes
320

321
        # xterm published application
322
        echo '[Desktop Entry]' > /usr/share/applications/xterm.desktop
323
        echo 'Name=XTerm' >> /usr/share/applications/xterm.desktop
324
        echo 'Comment=standard terminal emulator for the X window system' >> \
325
            /usr/share/applications/xterm.desktop
326
        echo 'Exec=xterm' >> /usr/share/applications/xterm.desktop
327
        echo 'Terminal=false' >> /usr/share/applications/xterm.desktop
328
        echo 'Type=Application' >> /usr/share/applications/xterm.desktop
329
        echo 'Encoding=UTF-8' >> /usr/share/applications/xterm.desktop
330
        echo 'Icon=xterm-color_48x48' >> /usr/share/applications/xterm.desktop
331
        echo 'Categories=System;TerminalEmulator;' >> \
332
                /usr/share/applications/xterm.desktop
333
        """.format(self.config.get('Global', 'apt_repo'))
334
        _run(cmd, False)
335

    
336
    def _find_flavor(self, flavor=None):
337
        """Find a suitable flavor to use
338

339
        Search by name (reg expression) or by id
340
        """
341
        # Get a list of flavors from config file
342
        flavors = self.config.get('Deployment', 'flavors').split(",")
343
        if flavor is not None:
344
            # If we have a flavor_name to use, add it to our list
345
            flavors.insert(0, flavor)
346

    
347
        list_flavors = self.compute_client.list_flavors()
348
        for flv in flavors:
349
            flv_type, flv_value = parse_typed_option(option="flavor",
350
                                                     value=flv)
351
            if flv_type == "name":
352
                # Filter flavors by name
353
                self.logger.debug(
354
                    "Trying to find a flavor with name \"%s\"" % flv_value)
355
                list_flvs = \
356
                    [f for f in list_flavors
357
                     if re.search(flv_value, f['name'], flags=re.I)
358
                     is not None]
359
            elif flv_type == "id":
360
                # Filter flavors by id
361
                self.logger.debug(
362
                    "Trying to find a flavor with id \"%s\"" % flv_value)
363
                list_flvs = \
364
                    [f for f in list_flavors
365
                     if str(f['id']) == flv_value]
366
            else:
367
                self.logger.error("Unrecognized flavor type %s" % flv_type)
368

    
369
            # Check if we found one
370
            if list_flvs:
371
                self.logger.debug("Will use \"%s\" with id \"%s\""
372
                                  % (list_flvs[0]['name'], list_flvs[0]['id']))
373
                return list_flvs[0]['id']
374

    
375
        self.logger.error("No matching flavor found.. aborting")
376
        sys.exit(1)
377

    
378
    def _find_image(self, image=None):
379
        """Find a suitable image to use
380

381
        In case of search by name, the image has to belong to one
382
        of the `DEFAULT_SYSTEM_IMAGES_UUID' users.
383
        In case of search by id it only has to exist.
384
        """
385
        # Get a list of images from config file
386
        images = self.config.get('Deployment', 'images').split(",")
387
        if image is not None:
388
            # If we have an image from command line, add it to our list
389
            images.insert(0, image)
390

    
391
        auth = self.astakos_client.authenticate()
392
        user_uuid = auth["access"]["token"]["tenant"]["id"]
393
        list_images = self.image_client.list_public(detail=True)['images']
394
        for img in images:
395
            img_type, img_value = parse_typed_option(option="image", value=img)
396
            if img_type == "name":
397
                # Filter images by name
398
                self.logger.debug(
399
                    "Trying to find an image with name \"%s\"" % img_value)
400
                accepted_uuids = DEFAULT_SYSTEM_IMAGES_UUID + [user_uuid]
401
                list_imgs = \
402
                    [i for i in list_images if i['user_id'] in accepted_uuids
403
                     and
404
                     re.search(img_value, i['name'], flags=re.I) is not None]
405
            elif img_type == "id":
406
                # Filter images by id
407
                self.logger.debug(
408
                    "Trying to find an image with id \"%s\"" % img_value)
409
                list_imgs = \
410
                    [i for i in list_images
411
                     if i['id'].lower() == img_value.lower()]
412
            else:
413
                self.logger.error("Unrecognized image type %s" % img_type)
414
                sys.exit(1)
415

    
416
            # Check if we found one
417
            if list_imgs:
418
                self.logger.debug("Will use \"%s\" with id \"%s\""
419
                                  % (list_imgs[0]['name'], list_imgs[0]['id']))
420
                return list_imgs[0]['id']
421

    
422
        # We didn't found one
423
        self.logger.error("No matching image found.. aborting")
424
        sys.exit(1)
425

    
426
    def _get_server_ip_and_port(self, server):
427
        """Compute server's IPv4 and ssh port number"""
428
        self.logger.info("Get server connection details..")
429
        server_ip = server['attachments'][0]['ipv4']
430
        if (".okeanos.io" in self.cyclades_client.base_url or
431
           ".demo.synnefo.org" in self.cyclades_client.base_url):
432
            tmp1 = int(server_ip.split(".")[2])
433
            tmp2 = int(server_ip.split(".")[3])
434
            server_ip = "gate.okeanos.io"
435
            server_port = 10000 + tmp1 * 256 + tmp2
436
        else:
437
            server_port = 22
438
        self.write_temp_config('server_ip', server_ip)
439
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
440
        self.write_temp_config('server_port', server_port)
441
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
442
        self.logger.debug("Access server using \"ssh -p %s %s@%s\"" %
443
                          (server_port, server['metadata']['users'],
444
                           server_ip))
445

    
446
    @_check_fabric
447
    def _copy_ssh_keys(self, ssh_keys):
448
        """Upload/Install ssh keys to server"""
449
        self.logger.debug("Check for authentication keys to use")
450
        if ssh_keys is None:
451
            ssh_keys = self.config.get("Deployment", "ssh_keys")
452

    
453
        if ssh_keys != "":
454
            ssh_keys = os.path.expanduser(ssh_keys)
455
            self.logger.debug("Will use %s authentication keys file" %
456
                              ssh_keys)
457
            keyfile = '/tmp/%s.pub' % fabric.env.user
458
            _run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False)
459
            if ssh_keys.startswith("http://") or \
460
                    ssh_keys.startswith("https://") or \
461
                    ssh_keys.startswith("ftp://"):
462
                cmd = """
463
                apt-get update
464
                apt-get install wget --yes --force-yes
465
                wget {0} -O {1} --no-check-certificate
466
                """.format(ssh_keys, keyfile)
467
                _run(cmd, False)
468
            elif os.path.exists(ssh_keys):
469
                _put(ssh_keys, keyfile)
470
            else:
471
                self.logger.debug("No ssh keys found")
472
                return
473
            _run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False)
474
            _run('rm %s' % keyfile, False)
475
            self.logger.debug("Uploaded ssh authorized keys")
476
        else:
477
            self.logger.debug("No ssh keys found")
478

    
479
    def write_temp_config(self, option, value):
480
        """Write changes back to config file"""
481
        # Acquire the lock to write to temp_config_file
482
        with filelocker.lock("%s.lock" % self.temp_config_file,
483
                             filelocker.LOCK_EX):
484

    
485
            # Read temp_config again to get any new entries
486
            self.temp_config.read(self.temp_config_file)
487

    
488
            # If build_id section doesn't exist create a new one
489
            try:
490
                self.temp_config.add_section(str(self.build_id))
491
                creation_time = \
492
                    time.strftime("%a, %d %b %Y %X", time.localtime())
493
                self.temp_config.set(str(self.build_id),
494
                                     "created", str(creation_time))
495
            except DuplicateSectionError:
496
                pass
497
            self.temp_config.set(str(self.build_id), option, str(value))
498
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
499
            self.temp_config.set(str(self.build_id), "modified", curr_time)
500
            with open(self.temp_config_file, 'wb') as tcf:
501
                self.temp_config.write(tcf)
502

    
503
    def read_temp_config(self, option):
504
        """Read from temporary_config file"""
505
        # If build_id is None use the latest one
506
        if self.build_id is None:
507
            ids = self.temp_config.sections()
508
            if ids:
509
                self.build_id = int(ids[-1])
510
            else:
511
                self.logger.error("No sections in temporary config file")
512
                sys.exit(1)
513
            self.logger.debug("Will use \"%s\" as build id"
514
                              % _green(self.build_id))
515
        # Read specified option
516
        return self.temp_config.get(str(self.build_id), option)
517

    
518
    def setup_fabric(self):
519
        """Setup fabric environment"""
520
        self.logger.info("Setup fabric parameters..")
521
        fabric.env.user = self.read_temp_config('server_user')
522
        fabric.env.host_string = self.read_temp_config('server_ip')
523
        fabric.env.port = int(self.read_temp_config('server_port'))
524
        fabric.env.password = self.read_temp_config('server_passwd')
525
        fabric.env.connection_attempts = 10
526
        fabric.env.shell = "/bin/bash -c"
527
        fabric.env.disable_known_hosts = True
528
        fabric.env.output_prefix = None
529

    
530
    def _check_hash_sum(self, localfile, remotefile):
531
        """Check hash sums of two files"""
532
        self.logger.debug("Check hash sum for local file %s" % localfile)
533
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
534
        self.logger.debug("Local file has sha256 hash %s" % hash1)
535
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
536
        hash2 = _run("sha256sum %s" % remotefile, False)
537
        hash2 = hash2.split(' ')[0]
538
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
539
        if hash1 != hash2:
540
            self.logger.error("Hashes differ.. aborting")
541
            sys.exit(-1)
542

    
543
    @_check_fabric
544
    def clone_repo(self, local_repo=False):
545
        """Clone Synnefo repo from slave server"""
546
        self.logger.info("Configure repositories on remote server..")
547
        self.logger.debug("Install/Setup git")
548
        cmd = """
549
        apt-get install git --yes --force-yes
550
        git config --global user.name {0}
551
        git config --global user.email {1}
552
        """.format(self.config.get('Global', 'git_config_name'),
553
                   self.config.get('Global', 'git_config_mail'))
554
        _run(cmd, False)
555

    
556
        # Find synnefo_repo and synnefo_branch to use
557
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
558
        synnefo_branch = self.config.get("Global", "synnefo_branch")
559
        if synnefo_branch == "":
560
            synnefo_branch = \
561
                subprocess.Popen(
562
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
563
                    stdout=subprocess.PIPE).communicate()[0].strip()
564
            if synnefo_branch == "HEAD":
565
                synnefo_branch = \
566
                    subprocess.Popen(
567
                        ["git", "rev-parse", "--short", "HEAD"],
568
                        stdout=subprocess.PIPE).communicate()[0].strip()
569
        self.logger.info("Will use branch %s" % synnefo_branch)
570

    
571
        if local_repo or synnefo_branch == "":
572
            # Use local_repo
573
            self.logger.debug("Push local repo to server")
574
            # Firstly create the remote repo
575
            _run("git init synnefo", False)
576
            # Then push our local repo over ssh
577
            # We have to pass some arguments to ssh command
578
            # namely to disable host checking.
579
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
580
            os.close(temp_ssh_file_handle)
581
            # XXX: git push doesn't read the password
582
            cmd = """
583
            echo 'exec ssh -o "StrictHostKeyChecking no" \
584
                           -o "UserKnownHostsFile /dev/null" \
585
                           -q "$@"' > {4}
586
            chmod u+x {4}
587
            export GIT_SSH="{4}"
588
            echo "{0}" | git push --mirror ssh://{1}@{2}:{3}/~/synnefo
589
            rm -f {4}
590
            """.format(fabric.env.password,
591
                       fabric.env.user,
592
                       fabric.env.host_string,
593
                       fabric.env.port,
594
                       temp_ssh_file)
595
            os.system(cmd)
596
        else:
597
            # Clone Synnefo from remote repo
598
            # Currently clonning synnefo can fail unexpectedly
599
            cloned = False
600
            for i in range(10):
601
                self.logger.debug("Clone synnefo from %s" % synnefo_repo)
602
                try:
603
                    _run("git clone %s synnefo" % synnefo_repo, False)
604
                    cloned = True
605
                    break
606
                except BaseException:
607
                    self.logger.warning(
608
                        "Clonning synnefo failed.. retrying %s" % i)
609
            if not cloned:
610
                self.logger.error("Can not clone Synnefo repo.")
611
                sys.exit(-1)
612

    
613
        # Checkout the desired synnefo_branch
614
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
615
        cmd = """
616
        cd synnefo
617
        for branch in `git branch -a | grep remotes | \
618
                       grep -v HEAD | grep -v master`; do
619
            git branch --track ${branch##*/} $branch
620
        done
621
        git checkout %s
622
        """ % (synnefo_branch)
623
        _run(cmd, False)
624

    
625
    @_check_fabric
626
    def build_synnefo(self):
627
        """Build Synnefo packages"""
628
        self.logger.info("Build Synnefo packages..")
629
        self.logger.debug("Install development packages")
630
        cmd = """
631
        apt-get update
632
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
633
                python-dev python-all python-pip --yes --force-yes
634
        pip install devflow
635
        """
636
        _run(cmd, False)
637

    
638
        if self.config.get('Global', 'patch_pydist') == "True":
639
            self.logger.debug("Patch pydist.py module")
640
            cmd = r"""
641
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
642
                /usr/share/python/debpython/pydist.py
643
            """
644
            _run(cmd, False)
645

646
        # Build synnefo packages
647
        self.logger.debug("Build synnefo packages")
648
        cmd = """
649
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
650
        """
651
        with fabric.cd("synnefo"):
652
            _run(cmd, True)
653

654
        # Install snf-deploy package
655
        self.logger.debug("Install snf-deploy package")
656
        cmd = """
657
        dpkg -i snf-deploy*.deb
658
        apt-get -f install --yes --force-yes
659
        """
660
        with fabric.cd("synnefo_build-area"):
661
            with fabric.settings(warn_only=True):
662
                _run(cmd, True)
663

664
        # Setup synnefo packages for snf-deploy
665
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
666
        cmd = """
667
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
668
        """
669
        _run(cmd, False)
670

671
    @_check_fabric
672
    def build_documentation(self):
673
        """Build Synnefo documentation"""
674
        self.logger.info("Build Synnefo documentation..")
675
        _run("pip install -U Sphinx", False)
676
        with fabric.cd("synnefo"):
677
            _run("devflow-update-version; "
678
                 "./ci/make_docs.sh synnefo_documentation", False)
679

680
    def fetch_documentation(self, dest=None):
681
        """Fetch Synnefo documentation"""
682
        self.logger.info("Fetch Synnefo documentation..")
683
        if dest is None:
684
            dest = "synnefo_documentation"
685
        dest = os.path.abspath(dest)
686
        if not os.path.exists(dest):
687
            os.makedirs(dest)
688
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
689
        self.logger.info("Downloaded documentation to %s" %
690
                         _green(dest))
691

692
    @_check_fabric
693
    def deploy_synnefo(self, schema=None):
694
        """Deploy Synnefo using snf-deploy"""
695
        self.logger.info("Deploy Synnefo..")
696
        if schema is None:
697
            schema = self.config.get('Global', 'schema')
698
        self.logger.debug("Will use \"%s\" schema" % schema)
699

700
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
701
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
702
            raise ValueError("Unknown schema: %s" % schema)
703

704
        self.logger.debug("Upload schema files to server")
705
        _put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/")
706

707
        self.logger.debug("Change password in nodes.conf file")
708
        cmd = """
709
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
710
        """.format(fabric.env.password)
711
        _run(cmd, False)
712

713
        self.logger.debug("Run snf-deploy")
714
        cmd = """
715
        snf-deploy keygen --force
716
        snf-deploy --disable-colors --autoconf all
717
        """
718
        _run(cmd, True)
719

720
    @_check_fabric
721
    def unit_test(self):
722
        """Run Synnefo unit test suite"""
723
        self.logger.info("Run Synnefo unit test suite")
724
        component = self.config.get('Unit Tests', 'component')
725

726
        self.logger.debug("Install needed packages")
727
        cmd = """
728
        pip install mock
729
        pip install factory_boy
730
        """
731
        _run(cmd, False)
732

733
        self.logger.debug("Upload tests.sh file")
734
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
735
        _put(unit_tests_file, ".")
736

737
        self.logger.debug("Run unit tests")
738
        cmd = """
739
        bash tests.sh {0}
740
        """.format(component)
741
        _run(cmd, True)
742

743
    @_check_fabric
744
    def run_burnin(self):
745
        """Run burnin functional test suite"""
746
        self.logger.info("Run Burnin functional test suite")
747
        cmd = """
748
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
749
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
750
        images_user=$(kamaki image list -l | grep owner | \
751
                      cut -d':' -f2 | tr -d ' ')
752
        snf-burnin --auth-url=$auth_url --token=$token \
753
            --force-flavor=2 --image-id=all \
754
            --system-images-user=$images_user \
755
            {0}
756
        BurninExitStatus=$?
757
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
758
        for i in $(ls $log_folder/*/details*); do
759
            echo -e "\\n\\n"
760
            echo -e "***** $i\\n"
761
            cat $i
762
        done
763
        exit $BurninExitStatus
764
        """.format(self.config.get('Burnin', 'cmd_options'))
765
        _run(cmd, True)
766

767
    @_check_fabric
768
    def fetch_compressed(self, src, dest=None):
769
        """Create a tarball and fetch it locally"""
770
        self.logger.debug("Creating tarball of %s" % src)
771
        basename = os.path.basename(src)
772
        tar_file = basename + ".tgz"
773
        cmd = "tar czf %s %s" % (tar_file, src)
774
        _run(cmd, False)
775
        if not os.path.exists(dest):
776
            os.makedirs(dest)
777

778
        tmp_dir = tempfile.mkdtemp()
779
        fabric.get(tar_file, tmp_dir)
780

781
        dest_file = os.path.join(tmp_dir, tar_file)
782
        self._check_hash_sum(dest_file, tar_file)
783
        self.logger.debug("Untar packages file %s" % dest_file)
784
        cmd = """
785
        cd %s
786
        tar xzf %s
787
        cp -r %s/* %s
788
        rm -r %s
789
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
790
        os.system(cmd)
791
        self.logger.info("Downloaded %s to %s" %
792
                         (src, _green(dest)))
793

794
    @_check_fabric
795
    def fetch_packages(self, dest=None):
796
        """Fetch Synnefo packages"""
797
        if dest is None:
798
            dest = self.config.get('Global', 'pkgs_dir')
799
        dest = os.path.abspath(os.path.expanduser(dest))
800
        if not os.path.exists(dest):
801
            os.makedirs(dest)
802
        self.fetch_compressed("synnefo_build-area", dest)
803
        self.logger.info("Downloaded debian packages to %s" %
804
                         _green(dest))
805

806
    def x2go_plugin(self, dest=None):
807
        """Produce an html page which will use the x2goplugin
808

    
809
        Arguments:
810
          dest  -- The file where to save the page (String)
811

    
812
        """
813
        output_str = """
814
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
815
        <html>
816
        <head>
817
        <title>X2Go SynnefoCI Service</title>
818
        </head>
819
        <body onload="checkPlugin()">
820
        <div id="x2goplugin">
821
            <object
822
                src="location"
823
                type="application/x2go"
824
                name="x2goplugin"
825
                palette="background"
826
                height="100%"
827
                hspace="0"
828
                vspace="0"
829
                width="100%"
830
                x2goconfig="
831
                    session=X2Go-SynnefoCI-Session
832
                    server={0}
833
                    user={1}
834
                    sshport={2}
835
                    published=true
836
                    autologin=true
837
                ">
838
            </object>
839
        </div>
840
        </body>
841
        </html>
842
        """.format(self.read_temp_config('server_ip'),
843
                   self.read_temp_config('server_user'),
844
                   self.read_temp_config('server_port'))
845
        if dest is None:
846
            dest = self.config.get('Global', 'x2go_plugin_file')
847

848
        self.logger.info("Writting x2go plugin html file to %s" % dest)
849
        fid = open(dest, 'w')
850
        fid.write(output_str)
851
        fid.close()
852

853

854
def parse_typed_option(option, value):
855
    """Parsed typed options (flavors and images)"""
856
    try:
857
        [type_, val] = value.strip().split(':')
858
        if type_ not in ["id", "name"]:
859
            raise ValueError
860
        return type_, val
861
    except ValueError:
862
        msg = "Invalid %s format. Must be [id|name]:.+" % option
863
        raise ValueError(msg)
864