Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 09f7ad00

History | View | Annotate | Download (37.1 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
            self.kamaki_cloud = config.get_global("default_cloud")
189

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

    
197
        self.astakos_client = AstakosClient(auth_url, token)
198

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

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

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

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

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

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

    
255
        # Find a build_id to use
256
        self._create_new_build_id()
257

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
582
        # Clone synnefo_repo
583
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
584
        # Clone pithos-web-client
585
        self.clone_pithos_webclient_repo(synnefo_branch)
586

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

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

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

    
646
        return synnefo_branch
647

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

    
657
        # Clone pithos-webclient from remote repo
658
        self.logger.debug("Clone pithos-webclient from %s" %
659
                          pithos_webclient_repo)
660
        self._git_clone(pithos_webclient_repo)
661

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

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

    
705
    def _git_clone(self, repo):
706
        """Clone repo to remote server
707

708
        Currently clonning from code.grnet.gr can fail unexpectedly.
709
        So retry!!
710

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

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

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

745
        # Build synnefo packages
746
        self.build_synnefo()
747
        # Build pithos-web-client packages
748
        self.build_pithos_webclient()
749

750
    @_check_fabric
751
    def build_synnefo(self):
752
        """Build Synnefo packages"""
753
        self.logger.info("Build Synnefo packages..")
754

755
        cmd = """
756
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
757
        """
758
        with fabric.cd("synnefo"):
759
            _run(cmd, True)
760

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

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

778
    @_check_fabric
779
    def build_pithos_webclient(self):
780
        """Build pithos-web-client packages"""
781
        self.logger.info("Build pithos-web-client packages..")
782

783
        cmd = """
784
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
785
        """
786
        with fabric.cd("pithos-web-client"):
787
            _run(cmd, True)
788

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

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

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

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

825
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
826
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
827
            raise ValueError("Unknown schema: %s" % schema)
828

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

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

838
        self.logger.debug("Run snf-deploy")
839
        cmd = """
840
        snf-deploy keygen --force
841
        snf-deploy --disable-colors --autoconf all
842
        """
843
        _run(cmd, True)
844

845
    @_check_fabric
846
    def unit_test(self):
847
        """Run Synnefo unit test suite"""
848
        self.logger.info("Run Synnefo unit test suite")
849
        component = self.config.get('Unit Tests', 'component')
850

851
        self.logger.debug("Install needed packages")
852
        cmd = """
853
        pip install -U mock
854
        pip install -U factory_boy
855
        """
856
        _run(cmd, False)
857

858
        self.logger.debug("Upload tests.sh file")
859
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
860
        _put(unit_tests_file, ".")
861

862
        self.logger.debug("Run unit tests")
863
        cmd = """
864
        bash tests.sh {0}
865
        """.format(component)
866
        _run(cmd, True)
867

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

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

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

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

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

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

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

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

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

979

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