Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ fcfef550

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 = "new_config"
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 = DEFAULT_CONFIG_FILE
144
        if not os.path.isabs(config_file):
145
            config_file = os.path.join(self.ci_dir, config_file)
146
        self.config = ConfigParser()
147
        self.config.optionxform = str
148
        self.config.read(config_file)
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

854

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