Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ cb1060f2

History | View | Annotate | Download (37.3 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
        if build_id is not None:
159
            self.logger.info("Will use \"%s\" as build id" %
160
                             _green(self.build_id))
161

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

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

    
180
    def setup_kamaki(self):
181
        """Initialize kamaki
182

183
        Setup cyclades_client, image_client and compute_client
184
        """
185

    
186
        config = kamaki_config.Config()
187
        if self.kamaki_cloud is None:
188
            try:
189
                self.kamaki_cloud = config.get("global", "default_cloud")
190
            except AttributeError:
191
                # Compatibility with kamaki version <=0.10
192
                self.kamaki_cloud = config.get_global("default_cloud")
193

    
194
        self.logger.info("Setup kamaki client, using cloud '%s'.." %
195
                         self.kamaki_cloud)
196
        auth_url = config.get_cloud(self.kamaki_cloud, "url")
197
        self.logger.debug("Authentication URL is %s" % _green(auth_url))
198
        token = config.get_cloud(self.kamaki_cloud, "token")
199
        #self.logger.debug("Token is %s" % _green(token))
200

    
201
        self.astakos_client = AstakosClient(auth_url, token)
202

    
203
        cyclades_url = \
204
            self.astakos_client.get_service_endpoints('compute')['publicURL']
205
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
206
        self.cyclades_client = CycladesClient(cyclades_url, token)
207
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
208

    
209
        image_url = \
210
            self.astakos_client.get_service_endpoints('image')['publicURL']
211
        self.logger.debug("Images API url is %s" % _green(image_url))
212
        self.image_client = ImageClient(cyclades_url, token)
213
        self.image_client.CONNECTION_RETRY_LIMIT = 2
214

    
215
        compute_url = \
216
            self.astakos_client.get_service_endpoints('compute')['publicURL']
217
        self.logger.debug("Compute API url is %s" % _green(compute_url))
218
        self.compute_client = ComputeClient(compute_url, token)
219
        self.compute_client.CONNECTION_RETRY_LIMIT = 2
220

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

    
245
    @_check_kamaki
246
    def destroy_server(self, wait=True):
247
        """Destroy slave server"""
248
        server_id = int(self.read_temp_config('server_id'))
249
        self.logger.info("Destoying server with id %s " % server_id)
250
        self.cyclades_client.delete_server(server_id)
251
        if wait:
252
            self._wait_transition(server_id, "ACTIVE", "DELETED")
253

    
254
    @_check_kamaki
255
    def create_server(self, image=None, flavor=None, ssh_keys=None):
256
        """Create slave server"""
257
        self.logger.info("Create a new server..")
258

    
259
        # Find a build_id to use
260
        self._create_new_build_id()
261

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
415
            # Check if we found one
416
            if list_imgs:
417
                self.logger.debug("Will use \"%s\" with id \"%s\""
418
                                  % (_green(list_imgs[0]['name']),
419
                                     _green(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
        ssh_command = "ssh -p %s %s@%s" \
443
            % (server_port, server['metadata']['users'], server_ip)
444
        self.logger.debug("Access server using \"%s\"" %
445
                          (_green(ssh_command)))
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
                              _green(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 _create_new_build_id(self):
481
        """Find a uniq build_id to use"""
482
        with filelocker.lock("%s.lock" % self.temp_config_file,
483
                             filelocker.LOCK_EX):
484
            # Read temp_config again to get any new entries
485
            self.temp_config.read(self.temp_config_file)
486

    
487
            # Find a uniq build_id to use
488
            if self.build_id is None:
489
                ids = self.temp_config.sections()
490
                if ids:
491
                    max_id = int(max(self.temp_config.sections(), key=int))
492
                    self.build_id = max_id + 1
493
                else:
494
                    self.build_id = 1
495
            self.logger.debug("Will use \"%s\" as build id"
496
                              % _green(self.build_id))
497

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

    
512
            # Write changes back to temp config file
513
            with open(self.temp_config_file, 'wb') as tcf:
514
                self.temp_config.write(tcf)
515

    
516
    def write_temp_config(self, option, value):
517
        """Write changes back to config file"""
518
        # Acquire the lock to write to temp_config_file
519
        with filelocker.lock("%s.lock" % self.temp_config_file,
520
                             filelocker.LOCK_EX):
521

    
522
            # Read temp_config again to get any new entries
523
            self.temp_config.read(self.temp_config_file)
524

    
525
            self.temp_config.set(str(self.build_id), option, str(value))
526
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
527
            self.temp_config.set(str(self.build_id), "modified", curr_time)
528

    
529
            # Write changes back to temp config file
530
            with open(self.temp_config_file, 'wb') as tcf:
531
                self.temp_config.write(tcf)
532

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

    
548
    def setup_fabric(self):
549
        """Setup fabric environment"""
550
        self.logger.info("Setup fabric parameters..")
551
        fabric.env.user = self.read_temp_config('server_user')
552
        fabric.env.host_string = self.read_temp_config('server_ip')
553
        fabric.env.port = int(self.read_temp_config('server_port'))
554
        fabric.env.password = self.read_temp_config('server_passwd')
555
        fabric.env.connection_attempts = 10
556
        fabric.env.shell = "/bin/bash -c"
557
        fabric.env.disable_known_hosts = True
558
        fabric.env.output_prefix = None
559

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

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

    
586
        # Clone synnefo_repo
587
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
588
        # Clone pithos-web-client
589
        self.clone_pithos_webclient_repo(synnefo_branch)
590

    
591
    @_check_fabric
592
    def clone_synnefo_repo(self, local_repo=False):
593
        """Clone Synnefo repo to remote server"""
594
        # Find synnefo_repo and synnefo_branch to use
595
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
596
        synnefo_branch = self.config.get("Global", "synnefo_branch")
597
        if synnefo_branch == "":
598
            synnefo_branch = \
599
                subprocess.Popen(
600
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
601
                    stdout=subprocess.PIPE).communicate()[0].strip()
602
            if synnefo_branch == "HEAD":
603
                synnefo_branch = \
604
                    subprocess.Popen(
605
                        ["git", "rev-parse", "--short", "HEAD"],
606
                        stdout=subprocess.PIPE).communicate()[0].strip()
607
        self.logger.debug("Will use branch \"%s\"" % _green(synnefo_branch))
608

    
609
        if local_repo or synnefo_repo == "":
610
            # Use local_repo
611
            self.logger.debug("Push local repo to server")
612
            # Firstly create the remote repo
613
            _run("git init synnefo", False)
614
            # Then push our local repo over ssh
615
            # We have to pass some arguments to ssh command
616
            # namely to disable host checking.
617
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
618
            os.close(temp_ssh_file_handle)
619
            # XXX: git push doesn't read the password
620
            cmd = """
621
            echo 'exec ssh -o "StrictHostKeyChecking no" \
622
                           -o "UserKnownHostsFile /dev/null" \
623
                           -q "$@"' > {4}
624
            chmod u+x {4}
625
            export GIT_SSH="{4}"
626
            echo "{0}" | git push --quiet --mirror ssh://{1}@{2}:{3}/~/synnefo
627
            rm -f {4}
628
            """.format(fabric.env.password,
629
                       fabric.env.user,
630
                       fabric.env.host_string,
631
                       fabric.env.port,
632
                       temp_ssh_file)
633
            os.system(cmd)
634
        else:
635
            # Clone Synnefo from remote repo
636
            self.logger.debug("Clone synnefo from %s" % synnefo_repo)
637
            self._git_clone(synnefo_repo)
638

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

    
650
        return synnefo_branch
651

    
652
    @_check_fabric
653
    def clone_pithos_webclient_repo(self, synnefo_branch):
654
        """Clone Pithos WebClient repo to remote server"""
655
        # Find pithos_webclient_repo and pithos_webclient_branch to use
656
        pithos_webclient_repo = \
657
            self.config.get('Global', 'pithos_webclient_repo')
658
        pithos_webclient_branch = \
659
            self.config.get('Global', 'pithos_webclient_branch')
660

    
661
        # Clone pithos-webclient from remote repo
662
        self.logger.debug("Clone pithos-webclient from %s" %
663
                          pithos_webclient_repo)
664
        self._git_clone(pithos_webclient_repo)
665

    
666
        # Track all pithos-webclient branches
667
        cmd = """
668
        cd pithos-web-client
669
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
670
            git branch --track ${branch##*/} $branch > /dev/null 2>&1
671
        done
672
        git branch
673
        """
674
        webclient_branches = _run(cmd, False)
675
        webclient_branches = webclient_branches.split()
676

    
677
        # If we have pithos_webclient_branch in config file use this one
678
        # else try to use the same branch as synnefo_branch
679
        # else use an appropriate one.
680
        if pithos_webclient_branch == "":
681
            if synnefo_branch in webclient_branches:
682
                pithos_webclient_branch = synnefo_branch
683
            else:
684
                # If synnefo_branch starts with one of
685
                # 'master', 'hotfix'; use the master branch
686
                if synnefo_branch.startswith('master') or \
687
                        synnefo_branch.startswith('hotfix'):
688
                    pithos_webclient_branch = "master"
689
                # If synnefo_branch starts with one of
690
                # 'develop', 'feature'; use the develop branch
691
                elif synnefo_branch.startswith('develop') or \
692
                        synnefo_branch.startswith('feature'):
693
                    pithos_webclient_branch = "develop"
694
                else:
695
                    self.logger.waring(
696
                        "Cannot determine which pithos-web-client branch to "
697
                        "use based on \"%s\" synnefo branch. "
698
                        "Will use develop." % synnefo_branch)
699
                    pithos_webclient_branch = "develop"
700
        # Checkout branch
701
        self.logger.debug("Checkout \"%s\" branch" %
702
                          _green(pithos_webclient_branch))
703
        cmd = """
704
        cd pithos-web-client
705
        git checkout {0}
706
        """.format(pithos_webclient_branch)
707
        _run(cmd, False)
708

    
709
    def _git_clone(self, repo):
710
        """Clone repo to remote server
711

712
        Currently clonning from code.grnet.gr can fail unexpectedly.
713
        So retry!!
714

715
        """
716
        cloned = False
717
        for i in range(1, 11):
718
            try:
719
                _run("git clone %s" % repo, False)
720
                cloned = True
721
                break
722
            except BaseException:
723
                self.logger.warning("Clonning failed.. retrying %s/10" % i)
724
        if not cloned:
725
            self.logger.error("Can not clone repo.")
726
            sys.exit(1)
727

    
728
    @_check_fabric
729
    def build_packages(self):
730
        """Build packages needed by Synnefo software"""
731
        self.logger.info("Install development packages")
732
        cmd = """
733
        apt-get update
734
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
735
                python-dev python-all python-pip ant --yes --force-yes
736
        pip install -U devflow
737
        """
738
        _run(cmd, False)
739

    
740
        # Patch pydist bug
741
        if self.config.get('Global', 'patch_pydist') == "True":
742
            self.logger.debug("Patch pydist.py module")
743
            cmd = r"""
744
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
745
                /usr/share/python/debpython/pydist.py
746
            """
747
            _run(cmd, False)
748

749
        # Build synnefo packages
750
        self.build_synnefo()
751
        # Build pithos-web-client packages
752
        self.build_pithos_webclient()
753

754
    @_check_fabric
755
    def build_synnefo(self):
756
        """Build Synnefo packages"""
757
        self.logger.info("Build Synnefo packages..")
758

759
        cmd = """
760
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
761
        """
762
        with fabric.cd("synnefo"):
763
            _run(cmd, True)
764

765
        # Install snf-deploy package
766
        self.logger.debug("Install snf-deploy package")
767
        cmd = """
768
        dpkg -i snf-deploy*.deb
769
        apt-get -f install --yes --force-yes
770
        """
771
        with fabric.cd("synnefo_build-area"):
772
            with fabric.settings(warn_only=True):
773
                _run(cmd, True)
774

775
        # Setup synnefo packages for snf-deploy
776
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
777
        cmd = """
778
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
779
        """
780
        _run(cmd, False)
781

782
    @_check_fabric
783
    def build_pithos_webclient(self):
784
        """Build pithos-web-client packages"""
785
        self.logger.info("Build pithos-web-client packages..")
786

787
        cmd = """
788
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
789
        """
790
        with fabric.cd("pithos-web-client"):
791
            _run(cmd, True)
792

793
        # Setup pithos-web-client packages for snf-deploy
794
        self.logger.debug("Copy webclient debs to snf-deploy packages dir")
795
        cmd = """
796
        cp ~/webclient_build-area/*.deb /var/lib/snf-deploy/packages/
797
        """
798
        _run(cmd, False)
799

800
    @_check_fabric
801
    def build_documentation(self):
802
        """Build Synnefo documentation"""
803
        self.logger.info("Build Synnefo documentation..")
804
        _run("pip install -U Sphinx", False)
805
        with fabric.cd("synnefo"):
806
            _run("devflow-update-version; "
807
                 "./ci/make_docs.sh synnefo_documentation", False)
808

809
    def fetch_documentation(self, dest=None):
810
        """Fetch Synnefo documentation"""
811
        self.logger.info("Fetch Synnefo documentation..")
812
        if dest is None:
813
            dest = "synnefo_documentation"
814
        dest = os.path.abspath(dest)
815
        if not os.path.exists(dest):
816
            os.makedirs(dest)
817
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
818
        self.logger.info("Downloaded documentation to %s" %
819
                         _green(dest))
820

821
    @_check_fabric
822
    def deploy_synnefo(self, schema=None):
823
        """Deploy Synnefo using snf-deploy"""
824
        self.logger.info("Deploy Synnefo..")
825
        if schema is None:
826
            schema = self.config.get('Global', 'schema')
827
        self.logger.debug("Will use \"%s\" schema" % _green(schema))
828

829
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
830
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
831
            raise ValueError("Unknown schema: %s" % schema)
832

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

836
        self.logger.debug("Change password in nodes.conf file")
837
        cmd = """
838
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
839
        """.format(fabric.env.password)
840
        _run(cmd, False)
841

842
        self.logger.debug("Run snf-deploy")
843
        cmd = """
844
        snf-deploy keygen --force
845
        snf-deploy --disable-colors --autoconf all
846
        """
847
        _run(cmd, True)
848

849
    @_check_fabric
850
    def unit_test(self):
851
        """Run Synnefo unit test suite"""
852
        self.logger.info("Run Synnefo unit test suite")
853
        component = self.config.get('Unit Tests', 'component')
854

855
        self.logger.debug("Install needed packages")
856
        cmd = """
857
        pip install -U mock
858
        pip install -U factory_boy
859
        pip install -U nose
860
        """
861
        _run(cmd, False)
862

863
        self.logger.debug("Upload tests.sh file")
864
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
865
        _put(unit_tests_file, ".")
866

867
        self.logger.debug("Run unit tests")
868
        cmd = """
869
        bash tests.sh {0}
870
        """.format(component)
871
        _run(cmd, True)
872

873
    @_check_fabric
874
    def run_burnin(self):
875
        """Run burnin functional test suite"""
876
        self.logger.info("Run Burnin functional test suite")
877
        cmd = """
878
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
879
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
880
        images_user=$(kamaki image list -l | grep owner | \
881
                      cut -d':' -f2 | tr -d ' ')
882
        snf-burnin --auth-url=$auth_url --token=$token \
883
            --force-flavor=2 --image-id=all \
884
            --system-images-user=$images_user \
885
            {0}
886
        BurninExitStatus=$?
887
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
888
        for i in $(ls $log_folder/*/details*); do
889
            echo -e "\\n\\n"
890
            echo -e "***** $i\\n"
891
            cat $i
892
        done
893
        exit $BurninExitStatus
894
        """.format(self.config.get('Burnin', 'cmd_options'))
895
        _run(cmd, True)
896

897
    @_check_fabric
898
    def fetch_compressed(self, src, dest=None):
899
        """Create a tarball and fetch it locally"""
900
        self.logger.debug("Creating tarball of %s" % src)
901
        basename = os.path.basename(src)
902
        tar_file = basename + ".tgz"
903
        cmd = "tar czf %s %s" % (tar_file, src)
904
        _run(cmd, False)
905
        if not os.path.exists(dest):
906
            os.makedirs(dest)
907

908
        tmp_dir = tempfile.mkdtemp()
909
        fabric.get(tar_file, tmp_dir)
910

911
        dest_file = os.path.join(tmp_dir, tar_file)
912
        self._check_hash_sum(dest_file, tar_file)
913
        self.logger.debug("Untar packages file %s" % dest_file)
914
        cmd = """
915
        cd %s
916
        tar xzf %s
917
        cp -r %s/* %s
918
        rm -r %s
919
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
920
        os.system(cmd)
921
        self.logger.info("Downloaded %s to %s" %
922
                         (src, _green(dest)))
923

924
    @_check_fabric
925
    def fetch_packages(self, dest=None):
926
        """Fetch Synnefo packages"""
927
        if dest is None:
928
            dest = self.config.get('Global', 'pkgs_dir')
929
        dest = os.path.abspath(os.path.expanduser(dest))
930
        if not os.path.exists(dest):
931
            os.makedirs(dest)
932
        self.fetch_compressed("synnefo_build-area", dest)
933
        self.fetch_compressed("webclient_build-area", dest)
934
        self.logger.info("Downloaded debian packages to %s" %
935
                         _green(dest))
936

937
    def x2go_plugin(self, dest=None):
938
        """Produce an html page which will use the x2goplugin
939

    
940
        Arguments:
941
          dest  -- The file where to save the page (String)
942

    
943
        """
944
        output_str = """
945
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
946
        <html>
947
        <head>
948
        <title>X2Go SynnefoCI Service</title>
949
        </head>
950
        <body onload="checkPlugin()">
951
        <div id="x2goplugin">
952
            <object
953
                src="location"
954
                type="application/x2go"
955
                name="x2goplugin"
956
                palette="background"
957
                height="100%"
958
                hspace="0"
959
                vspace="0"
960
                width="100%"
961
                x2goconfig="
962
                    session=X2Go-SynnefoCI-Session
963
                    server={0}
964
                    user={1}
965
                    sshport={2}
966
                    published=true
967
                    autologin=true
968
                ">
969
            </object>
970
        </div>
971
        </body>
972
        </html>
973
        """.format(self.read_temp_config('server_ip'),
974
                   self.read_temp_config('server_user'),
975
                   self.read_temp_config('server_port'))
976
        if dest is None:
977
            dest = self.config.get('Global', 'x2go_plugin_file')
978

979
        self.logger.info("Writting x2go plugin html file to %s" % dest)
980
        fid = open(dest, 'w')
981
        fid.write(output_str)
982
        fid.close()
983

984

985
def parse_typed_option(option, value):
986
    """Parsed typed options (flavors and images)"""
987
    try:
988
        [type_, val] = value.strip().split(':')
989
        if type_ not in ["id", "name"]:
990
            raise ValueError
991
        return type_, val
992
    except ValueError:
993
        msg = "Invalid %s format. Must be [id|name]:.+" % option
994
        raise ValueError(msg)
995