Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 04660f63

History | View | Annotate | Download (33.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_wheezy.conf"
25
# Is our terminal a colorful one?
26
USE_COLORS = True
27
# UUID of owner of system images
28
DEFAULT_SYSTEM_IMAGES_UUID = [
29
    "25ecced9-bf53-4145-91ee-cf47377e9fb2",  # production (okeanos.grnet.gr)
30
    "04cbe33f-29b7-4ef1-94fb-015929e5fc06",  # testing (okeanos.io)
31
]
32

    
33

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

    
43

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

    
49

    
50
def _red(msg):
51
    """Red color"""
52
    ret = "\x1b[31m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
53
    return ret
54

    
55

    
56
def _yellow(msg):
57
    """Yellow color"""
58
    ret = "\x1b[33m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
59
    return ret
60

    
61

    
62
def _green(msg):
63
    """Green color"""
64
    ret = "\x1b[32m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
65
    return ret
66

    
67

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

    
78

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

    
89

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

    
106

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

    
114

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

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

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

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

    
136
        self.logger.addHandler(handler1)
137
        self.logger.addHandler(handler2)
138

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
253
        # Find a build_id to use
254
        self._create_new_build_id()
255

    
256
        # Find an image to use
257
        image_id = self._find_image(image)
258
        # Find a flavor to use
259
        flavor_id = self._find_flavor(flavor)
260

    
261
        # Create Server
262
        server_name = self.config.get("Deployment", "server_name")
263
        server = self.cyclades_client.create_server(
264
            "%s(BID: %s)" % (server_name, self.build_id),
265
            flavor_id,
266
            image_id)
267
        server_id = server['id']
268
        self.write_temp_config('server_id', server_id)
269
        self.logger.debug("Server got id %s" % _green(server_id))
270
        server_user = server['metadata']['users']
271
        self.write_temp_config('server_user', server_user)
272
        self.logger.debug("Server's admin user is %s" % _green(server_user))
273
        server_passwd = server['adminPass']
274
        self.write_temp_config('server_passwd', server_passwd)
275

    
276
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
277
        self._get_server_ip_and_port(server)
278
        self._copy_ssh_keys(ssh_keys)
279

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

    
296
        # Setup apt, download packages
297
        self.logger.debug("Setup apt. Install x2goserver and firefox")
298
        cmd = """
299
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
300
        apt-get update
301
        apt-get install curl --yes --force-yes
302
        echo -e "\n\n{0}" >> /etc/apt/sources.list
303
        # Synnefo repo's key
304
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
305

306
        # X2GO Key
307
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
308
        apt-get install x2go-keyring --yes --force-yes
309
        apt-get update
310
        apt-get install x2goserver x2goserver-xsession \
311
                iceweasel --yes --force-yes
312

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

    
328
    def _find_flavor(self, flavor=None):
329
        """Find a suitable flavor to use
330

331
        Search by name (reg expression) or by id
332
        """
333
        # Get a list of flavors from config file
334
        flavors = self.config.get('Deployment', 'flavors').split(",")
335
        if flavor is not None:
336
            # If we have a flavor_name to use, add it to our list
337
            flavors.insert(0, flavor)
338

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

    
361
            # Check if we found one
362
            if list_flvs:
363
                self.logger.debug("Will use \"%s\" with id \"%s\""
364
                                  % (_green(list_flvs[0]['name']),
365
                                     _green(list_flvs[0]['id'])))
366
                return list_flvs[0]['id']
367

    
368
        self.logger.error("No matching flavor found.. aborting")
369
        sys.exit(1)
370

    
371
    def _find_image(self, image=None):
372
        """Find a suitable image to use
373

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

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

    
409
            # Check if we found one
410
            if list_imgs:
411
                self.logger.debug("Will use \"%s\" with id \"%s\""
412
                                  % (_green(list_imgs[0]['name']),
413
                                     _green(list_imgs[0]['id'])))
414
                return list_imgs[0]['id']
415

    
416
        # We didn't found one
417
        self.logger.error("No matching image found.. aborting")
418
        sys.exit(1)
419

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

    
441
    @_check_fabric
442
    def _copy_ssh_keys(self, ssh_keys):
443
        """Upload/Install ssh keys to server"""
444
        self.logger.debug("Check for authentication keys to use")
445
        if ssh_keys is None:
446
            ssh_keys = self.config.get("Deployment", "ssh_keys")
447

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

    
474
    def _create_new_build_id(self):
475
        """Find a uniq build_id to use"""
476
        with filelocker.lock("%s.lock" % self.temp_config_file,
477
                             filelocker.LOCK_EX):
478
            # Read temp_config again to get any new entries
479
            self.temp_config.read(self.temp_config_file)
480

    
481
            # Find a uniq build_id to use
482
            if self.build_id is None:
483
                ids = self.temp_config.sections()
484
                if ids:
485
                    max_id = int(max(self.temp_config.sections(), key=int))
486
                    self.build_id = max_id + 1
487
                else:
488
                    self.build_id = 1
489
            self.logger.debug("Will use \"%s\" as build id"
490
                              % _green(self.build_id))
491

    
492
            # Create a new section
493
            try:
494
                self.temp_config.add_section(str(self.build_id))
495
            except DuplicateSectionError:
496
                msg = ("Build id \"%s\" already in use. " +
497
                       "Please use a uniq one or cleanup \"%s\" file.\n") \
498
                    % (self.build_id, self.temp_config_file)
499
                self.logger.error(msg)
500
                sys.exit(1)
501
            creation_time = \
502
                time.strftime("%a, %d %b %Y %X", time.localtime())
503
            self.temp_config.set(str(self.build_id),
504
                                 "created", str(creation_time))
505

    
506
            # Write changes back to temp config file
507
            with open(self.temp_config_file, 'wb') as tcf:
508
                self.temp_config.write(tcf)
509

    
510
    def write_temp_config(self, option, value):
511
        """Write changes back to config file"""
512
        # Acquire the lock to write to temp_config_file
513
        with filelocker.lock("%s.lock" % self.temp_config_file,
514
                             filelocker.LOCK_EX):
515

    
516
            # Read temp_config again to get any new entries
517
            self.temp_config.read(self.temp_config_file)
518

    
519
            self.temp_config.set(str(self.build_id), option, str(value))
520
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
521
            self.temp_config.set(str(self.build_id), "modified", curr_time)
522

    
523
            # Write changes back to temp config file
524
            with open(self.temp_config_file, 'wb') as tcf:
525
                self.temp_config.write(tcf)
526

    
527
    def read_temp_config(self, option):
528
        """Read from temporary_config file"""
529
        # If build_id is None use the latest one
530
        if self.build_id is None:
531
            ids = self.temp_config.sections()
532
            if ids:
533
                self.build_id = int(ids[-1])
534
            else:
535
                self.logger.error("No sections in temporary config file")
536
                sys.exit(1)
537
            self.logger.debug("Will use \"%s\" as build id"
538
                              % _green(self.build_id))
539
        # Read specified option
540
        return self.temp_config.get(str(self.build_id), option)
541

    
542
    def setup_fabric(self):
543
        """Setup fabric environment"""
544
        self.logger.info("Setup fabric parameters..")
545
        fabric.env.user = self.read_temp_config('server_user')
546
        fabric.env.host_string = self.read_temp_config('server_ip')
547
        fabric.env.port = int(self.read_temp_config('server_port'))
548
        fabric.env.password = self.read_temp_config('server_passwd')
549
        fabric.env.connection_attempts = 10
550
        fabric.env.shell = "/bin/bash -c"
551
        fabric.env.disable_known_hosts = True
552
        fabric.env.output_prefix = None
553

    
554
    def _check_hash_sum(self, localfile, remotefile):
555
        """Check hash sums of two files"""
556
        self.logger.debug("Check hash sum for local file %s" % localfile)
557
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
558
        self.logger.debug("Local file has sha256 hash %s" % hash1)
559
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
560
        hash2 = _run("sha256sum %s" % remotefile, False)
561
        hash2 = hash2.split(' ')[0]
562
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
563
        if hash1 != hash2:
564
            self.logger.error("Hashes differ.. aborting")
565
            sys.exit(-1)
566

    
567
    @_check_fabric
568
    def clone_repo(self, local_repo=False):
569
        """Clone Synnefo repo from slave server"""
570
        self.logger.info("Configure repositories on remote server..")
571
        self.logger.debug("Install/Setup git")
572
        cmd = """
573
        apt-get install git --yes --force-yes
574
        git config --global user.name {0}
575
        git config --global user.email {1}
576
        """.format(self.config.get('Global', 'git_config_name'),
577
                   self.config.get('Global', 'git_config_mail'))
578
        _run(cmd, False)
579

    
580
        # Find synnefo_repo and synnefo_branch to use
581
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
582
        synnefo_branch = self.config.get("Global", "synnefo_branch")
583
        if synnefo_branch == "":
584
            synnefo_branch = \
585
                subprocess.Popen(
586
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
587
                    stdout=subprocess.PIPE).communicate()[0].strip()
588
            if synnefo_branch == "HEAD":
589
                synnefo_branch = \
590
                    subprocess.Popen(
591
                        ["git", "rev-parse", "--short", "HEAD"],
592
                        stdout=subprocess.PIPE).communicate()[0].strip()
593
        self.logger.info("Will use branch \"%s\"" % _green(synnefo_branch))
594

    
595
        if local_repo or synnefo_branch == "":
596
            # Use local_repo
597
            self.logger.debug("Push local repo to server")
598
            # Firstly create the remote repo
599
            _run("git init synnefo", False)
600
            # Then push our local repo over ssh
601
            # We have to pass some arguments to ssh command
602
            # namely to disable host checking.
603
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
604
            os.close(temp_ssh_file_handle)
605
            # XXX: git push doesn't read the password
606
            cmd = """
607
            echo 'exec ssh -o "StrictHostKeyChecking no" \
608
                           -o "UserKnownHostsFile /dev/null" \
609
                           -q "$@"' > {4}
610
            chmod u+x {4}
611
            export GIT_SSH="{4}"
612
            echo "{0}" | git push --mirror ssh://{1}@{2}:{3}/~/synnefo
613
            rm -f {4}
614
            """.format(fabric.env.password,
615
                       fabric.env.user,
616
                       fabric.env.host_string,
617
                       fabric.env.port,
618
                       temp_ssh_file)
619
            os.system(cmd)
620
        else:
621
            # Clone Synnefo from remote repo
622
            # Currently clonning synnefo can fail unexpectedly
623
            cloned = False
624
            for i in range(10):
625
                self.logger.debug("Clone synnefo from %s" % synnefo_repo)
626
                try:
627
                    _run("git clone %s synnefo" % synnefo_repo, False)
628
                    cloned = True
629
                    break
630
                except BaseException:
631
                    self.logger.warning(
632
                        "Clonning synnefo failed.. retrying %s" % i)
633
            if not cloned:
634
                self.logger.error("Can not clone Synnefo repo.")
635
                sys.exit(-1)
636

    
637
        # Checkout the desired synnefo_branch
638
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
639
        cmd = """
640
        cd synnefo
641
        for branch in `git branch -a | grep remotes | \
642
                       grep -v HEAD | grep -v master`; do
643
            git branch --track ${branch##*/} $branch
644
        done
645
        git checkout %s
646
        """ % (synnefo_branch)
647
        _run(cmd, False)
648

    
649
    @_check_fabric
650
    def build_synnefo(self):
651
        """Build Synnefo packages"""
652
        self.logger.info("Build Synnefo packages..")
653
        self.logger.debug("Install development packages")
654
        cmd = """
655
        apt-get update
656
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
657
                python-dev python-all python-pip --yes --force-yes
658
        pip install -U devflow
659
        """
660
        _run(cmd, False)
661

    
662
        if self.config.get('Global', 'patch_pydist') == "True":
663
            self.logger.debug("Patch pydist.py module")
664
            cmd = r"""
665
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
666
                /usr/share/python/debpython/pydist.py
667
            """
668
            _run(cmd, False)
669

670
        # Build synnefo packages
671
        self.logger.debug("Build synnefo packages")
672
        cmd = """
673
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
674
        """
675
        with fabric.cd("synnefo"):
676
            _run(cmd, True)
677

678
        # Install snf-deploy package
679
        self.logger.debug("Install snf-deploy package")
680
        cmd = """
681
        dpkg -i snf-deploy*.deb
682
        apt-get -f install --yes --force-yes
683
        """
684
        with fabric.cd("synnefo_build-area"):
685
            with fabric.settings(warn_only=True):
686
                _run(cmd, True)
687

688
        # Setup synnefo packages for snf-deploy
689
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
690
        cmd = """
691
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
692
        """
693
        _run(cmd, False)
694

695
    @_check_fabric
696
    def build_documentation(self):
697
        """Build Synnefo documentation"""
698
        self.logger.info("Build Synnefo documentation..")
699
        _run("pip install -U Sphinx", False)
700
        with fabric.cd("synnefo"):
701
            _run("devflow-update-version; "
702
                 "./ci/make_docs.sh synnefo_documentation", False)
703

704
    def fetch_documentation(self, dest=None):
705
        """Fetch Synnefo documentation"""
706
        self.logger.info("Fetch Synnefo documentation..")
707
        if dest is None:
708
            dest = "synnefo_documentation"
709
        dest = os.path.abspath(dest)
710
        if not os.path.exists(dest):
711
            os.makedirs(dest)
712
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
713
        self.logger.info("Downloaded documentation to %s" %
714
                         _green(dest))
715

716
    @_check_fabric
717
    def deploy_synnefo(self, schema=None):
718
        """Deploy Synnefo using snf-deploy"""
719
        self.logger.info("Deploy Synnefo..")
720
        if schema is None:
721
            schema = self.config.get('Global', 'schema')
722
        self.logger.debug("Will use \"%s\" schema" % _green(schema))
723

724
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
725
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
726
            raise ValueError("Unknown schema: %s" % schema)
727

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

731
        self.logger.debug("Change password in nodes.conf file")
732
        cmd = """
733
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
734
        """.format(fabric.env.password)
735
        _run(cmd, False)
736

737
        self.logger.debug("Run snf-deploy")
738
        cmd = """
739
        snf-deploy keygen --force
740
        snf-deploy --disable-colors --autoconf all
741
        """
742
        _run(cmd, True)
743

744
    @_check_fabric
745
    def unit_test(self):
746
        """Run Synnefo unit test suite"""
747
        self.logger.info("Run Synnefo unit test suite")
748
        component = self.config.get('Unit Tests', 'component')
749

750
        self.logger.debug("Install needed packages")
751
        cmd = """
752
        pip install -U mock
753
        pip install -U factory_boy
754
        """
755
        _run(cmd, False)
756

757
        self.logger.debug("Upload tests.sh file")
758
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
759
        _put(unit_tests_file, ".")
760

761
        self.logger.debug("Run unit tests")
762
        cmd = """
763
        bash tests.sh {0}
764
        """.format(component)
765
        _run(cmd, True)
766

767
    @_check_fabric
768
    def run_burnin(self):
769
        """Run burnin functional test suite"""
770
        self.logger.info("Run Burnin functional test suite")
771
        cmd = """
772
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
773
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
774
        images_user=$(kamaki image list -l | grep owner | \
775
                      cut -d':' -f2 | tr -d ' ')
776
        snf-burnin --auth-url=$auth_url --token=$token \
777
            --force-flavor=2 --image-id=all \
778
            --system-images-user=$images_user \
779
            {0}
780
        BurninExitStatus=$?
781
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
782
        for i in $(ls $log_folder/*/details*); do
783
            echo -e "\\n\\n"
784
            echo -e "***** $i\\n"
785
            cat $i
786
        done
787
        exit $BurninExitStatus
788
        """.format(self.config.get('Burnin', 'cmd_options'))
789
        _run(cmd, True)
790

791
    @_check_fabric
792
    def fetch_compressed(self, src, dest=None):
793
        """Create a tarball and fetch it locally"""
794
        self.logger.debug("Creating tarball of %s" % src)
795
        basename = os.path.basename(src)
796
        tar_file = basename + ".tgz"
797
        cmd = "tar czf %s %s" % (tar_file, src)
798
        _run(cmd, False)
799
        if not os.path.exists(dest):
800
            os.makedirs(dest)
801

802
        tmp_dir = tempfile.mkdtemp()
803
        fabric.get(tar_file, tmp_dir)
804

805
        dest_file = os.path.join(tmp_dir, tar_file)
806
        self._check_hash_sum(dest_file, tar_file)
807
        self.logger.debug("Untar packages file %s" % dest_file)
808
        cmd = """
809
        cd %s
810
        tar xzf %s
811
        cp -r %s/* %s
812
        rm -r %s
813
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
814
        os.system(cmd)
815
        self.logger.info("Downloaded %s to %s" %
816
                         (src, _green(dest)))
817

818
    @_check_fabric
819
    def fetch_packages(self, dest=None):
820
        """Fetch Synnefo packages"""
821
        if dest is None:
822
            dest = self.config.get('Global', 'pkgs_dir')
823
        dest = os.path.abspath(os.path.expanduser(dest))
824
        if not os.path.exists(dest):
825
            os.makedirs(dest)
826
        self.fetch_compressed("synnefo_build-area", dest)
827
        self.logger.info("Downloaded debian packages to %s" %
828
                         _green(dest))
829

830
    def x2go_plugin(self, dest=None):
831
        """Produce an html page which will use the x2goplugin
832

    
833
        Arguments:
834
          dest  -- The file where to save the page (String)
835

    
836
        """
837
        output_str = """
838
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
839
        <html>
840
        <head>
841
        <title>X2Go SynnefoCI Service</title>
842
        </head>
843
        <body onload="checkPlugin()">
844
        <div id="x2goplugin">
845
            <object
846
                src="location"
847
                type="application/x2go"
848
                name="x2goplugin"
849
                palette="background"
850
                height="100%"
851
                hspace="0"
852
                vspace="0"
853
                width="100%"
854
                x2goconfig="
855
                    session=X2Go-SynnefoCI-Session
856
                    server={0}
857
                    user={1}
858
                    sshport={2}
859
                    published=true
860
                    autologin=true
861
                ">
862
            </object>
863
        </div>
864
        </body>
865
        </html>
866
        """.format(self.read_temp_config('server_ip'),
867
                   self.read_temp_config('server_user'),
868
                   self.read_temp_config('server_port'))
869
        if dest is None:
870
            dest = self.config.get('Global', 'x2go_plugin_file')
871

872
        self.logger.info("Writting x2go plugin html file to %s" % dest)
873
        fid = open(dest, 'w')
874
        fid.write(output_str)
875
        fid.close()
876

877

878
def parse_typed_option(option, value):
879
    """Parsed typed options (flavors and images)"""
880
    try:
881
        [type_, val] = value.strip().split(':')
882
        if type_ not in ["id", "name"]:
883
            raise ValueError
884
        return type_, val
885
    except ValueError:
886
        msg = "Invalid %s format. Must be [id|name]:.+" % option
887
        raise ValueError(msg)
888