Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 2945e7ed

History | View | Annotate | Download (37.5 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
                      server_name=None):
257
        """Create slave server"""
258
        self.logger.info("Create a new server..")
259

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

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

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

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

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

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

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

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

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

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

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

    
369
            # Check if we found one
370
            if list_flvs:
371
                self.logger.debug("Will use \"%s\" with id \"%s\""
372
                                  % (_green(list_flvs[0]['name']),
373
                                     _green(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
                                  % (_green(list_imgs[0]['name']),
421
                                     _green(list_imgs[0]['id'])))
422
                return list_imgs[0]['id']
423

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

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

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

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

    
482
    def _create_new_build_id(self):
483
        """Find a uniq build_id to use"""
484
        with filelocker.lock("%s.lock" % self.temp_config_file,
485
                             filelocker.LOCK_EX):
486
            # Read temp_config again to get any new entries
487
            self.temp_config.read(self.temp_config_file)
488

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
652
        return synnefo_branch
653

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

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

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

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

    
711
    def _git_clone(self, repo):
712
        """Clone repo to remote server
713

714
        Currently clonning from code.grnet.gr can fail unexpectedly.
715
        So retry!!
716

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

910
        tmp_dir = tempfile.mkdtemp()
911
        fabric.get(tar_file, tmp_dir)
912

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

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

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

    
942
        Arguments:
943
          dest  -- The file where to save the page (String)
944

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

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

986

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