Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 9f41ab1e

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
                      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
        kamaki_version = self.config.get('Burnin', 'kamaki_version')
735
        cmd = """
736
        apt-get update
737
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
738
                python-dev python-all python-pip ant --yes --force-yes
739
        pip install -U devflow kamaki{0}
740
        """.format(("==" + kamaki_version) if kamaki_version else "")
741
        _run(cmd, False)
742

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

876
    @_check_fabric
877
    def run_burnin(self):
878
        """Run burnin functional test suite"""
879
        self.logger.info("Run Burnin functional test suite")
880
        cmd = """
881
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
882
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
883
        images_user=$(kamaki image list -l | grep owner | \
884
                      cut -d':' -f2 | tr -d ' ')
885
        snf-burnin --auth-url=$auth_url --token=$token {0}
886
        BurninExitStatus=$?
887
        exit $BurninExitStatus
888
        """.format(self.config.get('Burnin', 'cmd_options'))
889
        _run(cmd, True)
890

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

902
        tmp_dir = tempfile.mkdtemp()
903
        fabric.get(tar_file, tmp_dir)
904

905
        dest_file = os.path.join(tmp_dir, tar_file)
906
        self._check_hash_sum(dest_file, tar_file)
907
        self.logger.debug("Untar packages file %s" % dest_file)
908
        cmd = """
909
        cd %s
910
        tar xzf %s
911
        cp -r %s/* %s
912
        rm -r %s
913
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
914
        os.system(cmd)
915
        self.logger.info("Downloaded %s to %s" %
916
                         (src, _green(dest)))
917

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

931
    def x2go_plugin(self, dest=None):
932
        """Produce an html page which will use the x2goplugin
933

    
934
        Arguments:
935
          dest  -- The file where to save the page (String)
936

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

973
        self.logger.info("Writting x2go plugin html file to %s" % dest)
974
        fid = open(dest, 'w')
975
        fid.write(output_str)
976
        fid.close()
977

978

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