Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 58e7347a

History | View | Annotate | Download (40.4 kB)

1
#!/usr/bin/env python
2

    
3
"""
4
Synnefo ci utils module
5
"""
6

    
7
import os
8
import re
9
import sys
10
import time
11
import logging
12
import fabric.api as fabric
13
import subprocess
14
import tempfile
15
from ConfigParser import ConfigParser, DuplicateSectionError
16

    
17
from kamaki.cli import config as kamaki_config
18
from kamaki.clients.astakos import AstakosClient, parse_endpoints
19
from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient
20
from kamaki.clients.image import ImageClient
21
from kamaki.clients.compute import ComputeClient
22
from kamaki.clients import ClientError
23
import filelocker
24

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

    
34

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

    
44

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

    
50

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

    
56

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

    
62

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

    
68

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

    
79

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

    
90

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

    
107

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

    
115

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

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

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

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

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

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

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

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

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

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

    
182
    def setup_kamaki(self):
183
        """Initialize kamaki
184

185
        Setup cyclades_client, image_client and compute_client
186
        """
187

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

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

    
203
        self.astakos_client = AstakosClient(auth_url, token)
204
        endpoints = self.astakos_client.authenticate()
205

    
206
        cyclades_url = get_endpoint_url(endpoints, "compute")
207
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
208
        self.cyclades_client = CycladesClient(cyclades_url, token)
209
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
210

    
211
        network_url = get_endpoint_url(endpoints, "network")
212
        self.logger.debug("Network API url is %s" % _green(network_url))
213
        self.network_client = CycladesNetworkClient(network_url, token)
214
        self.network_client.CONNECTION_RETRY_LIMIT = 2
215

    
216
        image_url = get_endpoint_url(endpoints, "image")
217
        self.logger.debug("Images API url is %s" % _green(image_url))
218
        self.image_client = ImageClient(cyclades_url, token)
219
        self.image_client.CONNECTION_RETRY_LIMIT = 2
220

    
221
        compute_url = get_endpoint_url(endpoints, "compute")
222
        self.logger.debug("Compute API url is %s" % _green(compute_url))
223
        self.compute_client = ComputeClient(compute_url, token)
224
        self.compute_client.CONNECTION_RETRY_LIMIT = 2
225

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

    
250
    @_check_kamaki
251
    def destroy_server(self, wait=True):
252
        """Destroy slave server"""
253
        server_id = int(self.read_temp_config('server_id'))
254
        fips = [f for f in self.network_client.list_floatingips()
255
                if str(f['instance_id']) == str(server_id)]
256
        self.logger.info("Destoying server with id %s " % server_id)
257
        self.cyclades_client.delete_server(server_id)
258
        if wait:
259
            self._wait_transition(server_id, "ACTIVE", "DELETED")
260
        for fip in fips:
261
            self.logger.info("Destroying floating ip %s",
262
                             fip['floating_ip_address'])
263
            self.network_client.delete_floatingip(fip['id'])
264

    
265
    def _create_floating_ip(self):
266
        """Create a new floating ip"""
267
        networks = self.network_client.list_networks(detail=True)
268
        pub_nets = [n for n in networks
269
                    if n['SNF:floating_ip_pool'] and n['public']]
270
        for pub_net in pub_nets:
271
            # Try until we find a public network that is not full
272
            try:
273
                fip = self.network_client.create_floatingip(pub_net['id'])
274
            except ClientError as err:
275
                self.logger.warning("%s: %s", err.message, err.details)
276
                continue
277
            self.logger.debug("Floating IP %s with id %s created",
278
                              fip['floating_ip_address'], fip['id'])
279
            return fip
280
        self.logger.error("No mor IP addresses available")
281
        sys.exit(1)
282

    
283
    def _create_port(self, floating_ip):
284
        """Create a new port for our floating IP"""
285
        net_id = floating_ip['floating_network_id']
286
        self.logger.debug("Creating a new port to network with id %s", net_id)
287
        fixed_ips = [{'ip_address': floating_ip['floating_ip_address']}]
288
        port = self.network_client.create_port(
289
            net_id, device_id=None, fixed_ips=fixed_ips)
290
        return port
291

    
292
    @_check_kamaki
293
    # Too many local variables. pylint: disable-msg=R0914
294
    def create_server(self, image=None, flavor=None, ssh_keys=None,
295
                      server_name=None):
296
        """Create slave server"""
297
        self.logger.info("Create a new server..")
298

    
299
        # Find a build_id to use
300
        self._create_new_build_id()
301

    
302
        # Find an image to use
303
        image_id = self._find_image(image)
304
        # Find a flavor to use
305
        flavor_id = self._find_flavor(flavor)
306

    
307
        # Create Server
308
        networks = []
309
        if self.config.get("Deployment", "allocate_floating_ip") == "True":
310
            fip = self._create_floating_ip()
311
            port = self._create_port(fip)
312
            networks.append({'port': port['id']})
313
        private_networks = self.config.get('Deployment', 'private_networks')
314
        if private_networks:
315
            private_networks = [p.strip() for p in private_networks.split(",")]
316
            networks.extend([{"uuid": uuid} for uuid in private_networks])
317
        if server_name is None:
318
            server_name = self.config.get("Deployment", "server_name")
319
            server_name = "%s(BID: %s)" % (server_name, self.build_id)
320
        server = self.cyclades_client.create_server(
321
            server_name, flavor_id, image_id, networks=networks)
322
        server_id = server['id']
323
        self.write_temp_config('server_id', server_id)
324
        self.logger.debug("Server got id %s" % _green(server_id))
325
        server_user = server['metadata']['users']
326
        self.write_temp_config('server_user', server_user)
327
        self.logger.debug("Server's admin user is %s" % _green(server_user))
328
        server_passwd = server['adminPass']
329
        self.write_temp_config('server_passwd', server_passwd)
330

    
331
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
332
        self._get_server_ip_and_port(server, private_networks)
333
        self._copy_ssh_keys(ssh_keys)
334

    
335
        # Setup Firewall
336
        self.setup_fabric()
337
        self.logger.info("Setup firewall")
338
        accept_ssh_from = self.config.get('Global', 'accept_ssh_from')
339
        if accept_ssh_from != "":
340
            self.logger.debug("Block ssh except from %s" % accept_ssh_from)
341
            cmd = """
342
            local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
343
                cut -d':' -f2 | cut -d' ' -f1)
344
            iptables -A INPUT -s localhost -j ACCEPT
345
            iptables -A INPUT -s $local_ip -j ACCEPT
346
            iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
347
            iptables -A INPUT -p tcp --dport 22 -j DROP
348
            """.format(accept_ssh_from)
349
            _run(cmd, False)
350

    
351
        # Setup apt, download packages
352
        self.logger.debug("Setup apt. Install x2goserver and firefox")
353
        cmd = """
354
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
355
        echo 'precedence ::ffff:0:0/96  100' >> /etc/gai.conf
356
        apt-get update
357
        apt-get install curl --yes --force-yes
358
        echo -e "\n\n{0}" >> /etc/apt/sources.list
359
        # Synnefo repo's key
360
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
361

362
        # X2GO Key
363
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
364
        apt-get install x2go-keyring --yes --force-yes
365
        apt-get update
366
        apt-get install x2goserver x2goserver-xsession \
367
                iceweasel --yes --force-yes
368

369
        # xterm published application
370
        echo '[Desktop Entry]' > /usr/share/applications/xterm.desktop
371
        echo 'Name=XTerm' >> /usr/share/applications/xterm.desktop
372
        echo 'Comment=standard terminal emulator for the X window system' >> \
373
            /usr/share/applications/xterm.desktop
374
        echo 'Exec=xterm' >> /usr/share/applications/xterm.desktop
375
        echo 'Terminal=false' >> /usr/share/applications/xterm.desktop
376
        echo 'Type=Application' >> /usr/share/applications/xterm.desktop
377
        echo 'Encoding=UTF-8' >> /usr/share/applications/xterm.desktop
378
        echo 'Icon=xterm-color_48x48' >> /usr/share/applications/xterm.desktop
379
        echo 'Categories=System;TerminalEmulator;' >> \
380
                /usr/share/applications/xterm.desktop
381
        """.format(self.config.get('Global', 'apt_repo'))
382
        _run(cmd, False)
383

    
384
    def _find_flavor(self, flavor=None):
385
        """Find a suitable flavor to use
386

387
        Search by name (reg expression) or by id
388
        """
389
        # Get a list of flavors from config file
390
        flavors = self.config.get('Deployment', 'flavors').split(",")
391
        if flavor is not None:
392
            # If we have a flavor_name to use, add it to our list
393
            flavors.insert(0, flavor)
394

    
395
        list_flavors = self.compute_client.list_flavors()
396
        for flv in flavors:
397
            flv_type, flv_value = parse_typed_option(option="flavor",
398
                                                     value=flv)
399
            if flv_type == "name":
400
                # Filter flavors by name
401
                self.logger.debug(
402
                    "Trying to find a flavor with name \"%s\"" % flv_value)
403
                list_flvs = \
404
                    [f for f in list_flavors
405
                     if re.search(flv_value, f['name'], flags=re.I)
406
                     is not None]
407
            elif flv_type == "id":
408
                # Filter flavors by id
409
                self.logger.debug(
410
                    "Trying to find a flavor with id \"%s\"" % flv_value)
411
                list_flvs = \
412
                    [f for f in list_flavors
413
                     if str(f['id']) == flv_value]
414
            else:
415
                self.logger.error("Unrecognized flavor type %s" % flv_type)
416

    
417
            # Check if we found one
418
            if list_flvs:
419
                self.logger.debug("Will use \"%s\" with id \"%s\""
420
                                  % (_green(list_flvs[0]['name']),
421
                                     _green(list_flvs[0]['id'])))
422
                return list_flvs[0]['id']
423

    
424
        self.logger.error("No matching flavor found.. aborting")
425
        sys.exit(1)
426

    
427
    def _find_image(self, image=None):
428
        """Find a suitable image to use
429

430
        In case of search by name, the image has to belong to one
431
        of the `DEFAULT_SYSTEM_IMAGES_UUID' users.
432
        In case of search by id it only has to exist.
433
        """
434
        # Get a list of images from config file
435
        images = self.config.get('Deployment', 'images').split(",")
436
        if image is not None:
437
            # If we have an image from command line, add it to our list
438
            images.insert(0, image)
439

    
440
        auth = self.astakos_client.authenticate()
441
        user_uuid = auth["access"]["token"]["tenant"]["id"]
442
        list_images = self.image_client.list_public(detail=True)['images']
443
        for img in images:
444
            img_type, img_value = parse_typed_option(option="image", value=img)
445
            if img_type == "name":
446
                # Filter images by name
447
                self.logger.debug(
448
                    "Trying to find an image with name \"%s\"" % img_value)
449
                accepted_uuids = DEFAULT_SYSTEM_IMAGES_UUID + [user_uuid]
450
                list_imgs = \
451
                    [i for i in list_images if i['user_id'] in accepted_uuids
452
                     and
453
                     re.search(img_value, i['name'], flags=re.I) is not None]
454
            elif img_type == "id":
455
                # Filter images by id
456
                self.logger.debug(
457
                    "Trying to find an image with id \"%s\"" % img_value)
458
                list_imgs = \
459
                    [i for i in list_images
460
                     if i['id'].lower() == img_value.lower()]
461
            else:
462
                self.logger.error("Unrecognized image type %s" % img_type)
463
                sys.exit(1)
464

    
465
            # Check if we found one
466
            if list_imgs:
467
                self.logger.debug("Will use \"%s\" with id \"%s\""
468
                                  % (_green(list_imgs[0]['name']),
469
                                     _green(list_imgs[0]['id'])))
470
                return list_imgs[0]['id']
471

    
472
        # We didn't found one
473
        self.logger.error("No matching image found.. aborting")
474
        sys.exit(1)
475

    
476
    def _get_server_ip_and_port(self, server, private_networks):
477
        """Compute server's IPv4 and ssh port number"""
478
        self.logger.info("Get server connection details..")
479
        if private_networks:
480
            # Choose the networks that belong to private_networks
481
            networks = [n for n in server['attachments']
482
                        if n['network_id'] in private_networks]
483
        else:
484
            # Choose the networks that are public
485
            networks = [n for n in server['attachments']
486
                        if self.network_client.
487
                        get_network_details(n['network_id'])['public']]
488
        # Choose the networks with IPv4
489
        networks = [n for n in networks if n['ipv4']]
490
        # Use the first network as IPv4
491
        server_ip = networks[0]['ipv4']
492

    
493
        if (".okeanos.io" in self.cyclades_client.base_url or
494
           ".demo.synnefo.org" in self.cyclades_client.base_url):
495
            tmp1 = int(server_ip.split(".")[2])
496
            tmp2 = int(server_ip.split(".")[3])
497
            server_ip = "gate.okeanos.io"
498
            server_port = 10000 + tmp1 * 256 + tmp2
499
        else:
500
            server_port = 22
501
        self.write_temp_config('server_ip', server_ip)
502
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
503
        self.write_temp_config('server_port', server_port)
504
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
505
        ssh_command = "ssh -p %s %s@%s" \
506
            % (server_port, server['metadata']['users'], server_ip)
507
        self.logger.debug("Access server using \"%s\"" %
508
                          (_green(ssh_command)))
509

    
510
    @_check_fabric
511
    def _copy_ssh_keys(self, ssh_keys):
512
        """Upload/Install ssh keys to server"""
513
        self.logger.debug("Check for authentication keys to use")
514
        if ssh_keys is None:
515
            ssh_keys = self.config.get("Deployment", "ssh_keys")
516

    
517
        if ssh_keys != "":
518
            ssh_keys = os.path.expanduser(ssh_keys)
519
            self.logger.debug("Will use \"%s\" authentication keys file" %
520
                              _green(ssh_keys))
521
            keyfile = '/tmp/%s.pub' % fabric.env.user
522
            _run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False)
523
            if ssh_keys.startswith("http://") or \
524
                    ssh_keys.startswith("https://") or \
525
                    ssh_keys.startswith("ftp://"):
526
                cmd = """
527
                apt-get update
528
                apt-get install wget --yes --force-yes
529
                wget {0} -O {1} --no-check-certificate
530
                """.format(ssh_keys, keyfile)
531
                _run(cmd, False)
532
            elif os.path.exists(ssh_keys):
533
                _put(ssh_keys, keyfile)
534
            else:
535
                self.logger.debug("No ssh keys found")
536
                return
537
            _run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False)
538
            _run('rm %s' % keyfile, False)
539
            self.logger.debug("Uploaded ssh authorized keys")
540
        else:
541
            self.logger.debug("No ssh keys found")
542

    
543
    def _create_new_build_id(self):
544
        """Find a uniq build_id to use"""
545
        with filelocker.lock("%s.lock" % self.temp_config_file,
546
                             filelocker.LOCK_EX):
547
            # Read temp_config again to get any new entries
548
            self.temp_config.read(self.temp_config_file)
549

    
550
            # Find a uniq build_id to use
551
            if self.build_id is None:
552
                ids = self.temp_config.sections()
553
                if ids:
554
                    max_id = int(max(self.temp_config.sections(), key=int))
555
                    self.build_id = max_id + 1
556
                else:
557
                    self.build_id = 1
558
            self.logger.debug("Will use \"%s\" as build id"
559
                              % _green(self.build_id))
560

    
561
            # Create a new section
562
            try:
563
                self.temp_config.add_section(str(self.build_id))
564
            except DuplicateSectionError:
565
                msg = ("Build id \"%s\" already in use. " +
566
                       "Please use a uniq one or cleanup \"%s\" file.\n") \
567
                    % (self.build_id, self.temp_config_file)
568
                self.logger.error(msg)
569
                sys.exit(1)
570
            creation_time = \
571
                time.strftime("%a, %d %b %Y %X", time.localtime())
572
            self.temp_config.set(str(self.build_id),
573
                                 "created", str(creation_time))
574

    
575
            # Write changes back to temp config file
576
            with open(self.temp_config_file, 'wb') as tcf:
577
                self.temp_config.write(tcf)
578

    
579
    def write_temp_config(self, option, value):
580
        """Write changes back to config file"""
581
        # Acquire the lock to write to temp_config_file
582
        with filelocker.lock("%s.lock" % self.temp_config_file,
583
                             filelocker.LOCK_EX):
584

    
585
            # Read temp_config again to get any new entries
586
            self.temp_config.read(self.temp_config_file)
587

    
588
            self.temp_config.set(str(self.build_id), option, str(value))
589
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
590
            self.temp_config.set(str(self.build_id), "modified", curr_time)
591

    
592
            # Write changes back to temp config file
593
            with open(self.temp_config_file, 'wb') as tcf:
594
                self.temp_config.write(tcf)
595

    
596
    def read_temp_config(self, option):
597
        """Read from temporary_config file"""
598
        # If build_id is None use the latest one
599
        if self.build_id is None:
600
            ids = self.temp_config.sections()
601
            if ids:
602
                self.build_id = int(ids[-1])
603
            else:
604
                self.logger.error("No sections in temporary config file")
605
                sys.exit(1)
606
            self.logger.debug("Will use \"%s\" as build id"
607
                              % _green(self.build_id))
608
        # Read specified option
609
        return self.temp_config.get(str(self.build_id), option)
610

    
611
    def setup_fabric(self):
612
        """Setup fabric environment"""
613
        self.logger.info("Setup fabric parameters..")
614
        fabric.env.user = self.read_temp_config('server_user')
615
        fabric.env.host_string = self.read_temp_config('server_ip')
616
        fabric.env.port = int(self.read_temp_config('server_port'))
617
        fabric.env.password = self.read_temp_config('server_passwd')
618
        fabric.env.connection_attempts = 10
619
        fabric.env.shell = "/bin/bash -c"
620
        fabric.env.disable_known_hosts = True
621
        fabric.env.output_prefix = None
622

    
623
    def _check_hash_sum(self, localfile, remotefile):
624
        """Check hash sums of two files"""
625
        self.logger.debug("Check hash sum for local file %s" % localfile)
626
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
627
        self.logger.debug("Local file has sha256 hash %s" % hash1)
628
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
629
        hash2 = _run("sha256sum %s" % remotefile, False)
630
        hash2 = hash2.split(' ')[0]
631
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
632
        if hash1 != hash2:
633
            self.logger.error("Hashes differ.. aborting")
634
            sys.exit(1)
635

    
636
    @_check_fabric
637
    def clone_repo(self, local_repo=False):
638
        """Clone Synnefo repo from slave server"""
639
        self.logger.info("Configure repositories on remote server..")
640
        self.logger.debug("Install/Setup git")
641
        cmd = """
642
        apt-get install git --yes --force-yes
643
        git config --global user.name {0}
644
        git config --global user.email {1}
645
        """.format(self.config.get('Global', 'git_config_name'),
646
                   self.config.get('Global', 'git_config_mail'))
647
        _run(cmd, False)
648

    
649
        # Clone synnefo_repo
650
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
651
        # Clone pithos-web-client
652
        self.clone_pithos_webclient_repo(synnefo_branch)
653

    
654
    @_check_fabric
655
    def clone_synnefo_repo(self, local_repo=False):
656
        """Clone Synnefo repo to remote server"""
657
        # Find synnefo_repo and synnefo_branch to use
658
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
659
        synnefo_branch = self.config.get("Global", "synnefo_branch")
660
        if synnefo_branch == "":
661
            synnefo_branch = \
662
                subprocess.Popen(
663
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
664
                    stdout=subprocess.PIPE).communicate()[0].strip()
665
            if synnefo_branch == "HEAD":
666
                synnefo_branch = \
667
                    subprocess.Popen(
668
                        ["git", "rev-parse", "--short", "HEAD"],
669
                        stdout=subprocess.PIPE).communicate()[0].strip()
670
        self.logger.debug("Will use branch \"%s\"" % _green(synnefo_branch))
671

    
672
        if local_repo or synnefo_repo == "":
673
            # Use local_repo
674
            self.logger.debug("Push local repo to server")
675
            # Firstly create the remote repo
676
            _run("git init synnefo", False)
677
            # Then push our local repo over ssh
678
            # We have to pass some arguments to ssh command
679
            # namely to disable host checking.
680
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
681
            os.close(temp_ssh_file_handle)
682
            # XXX: git push doesn't read the password
683
            cmd = """
684
            echo 'exec ssh -o "StrictHostKeyChecking no" \
685
                           -o "UserKnownHostsFile /dev/null" \
686
                           -q "$@"' > {4}
687
            chmod u+x {4}
688
            export GIT_SSH="{4}"
689
            echo "{0}" | git push --quiet --mirror ssh://{1}@{2}:{3}/~/synnefo
690
            rm -f {4}
691
            """.format(fabric.env.password,
692
                       fabric.env.user,
693
                       fabric.env.host_string,
694
                       fabric.env.port,
695
                       temp_ssh_file)
696
            os.system(cmd)
697
        else:
698
            # Clone Synnefo from remote repo
699
            self.logger.debug("Clone synnefo from %s" % synnefo_repo)
700
            self._git_clone(synnefo_repo)
701

    
702
        # Checkout the desired synnefo_branch
703
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
704
        cmd = """
705
        cd synnefo
706
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
707
            git branch --track ${branch##*/} $branch
708
        done
709
        git checkout %s
710
        """ % (synnefo_branch)
711
        _run(cmd, False)
712

    
713
        return synnefo_branch
714

    
715
    @_check_fabric
716
    def clone_pithos_webclient_repo(self, synnefo_branch):
717
        """Clone Pithos WebClient repo to remote server"""
718
        # Find pithos_webclient_repo and pithos_webclient_branch to use
719
        pithos_webclient_repo = \
720
            self.config.get('Global', 'pithos_webclient_repo')
721
        pithos_webclient_branch = \
722
            self.config.get('Global', 'pithos_webclient_branch')
723

    
724
        # Clone pithos-webclient from remote repo
725
        self.logger.debug("Clone pithos-webclient from %s" %
726
                          pithos_webclient_repo)
727
        self._git_clone(pithos_webclient_repo)
728

    
729
        # Track all pithos-webclient branches
730
        cmd = """
731
        cd pithos-web-client
732
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
733
            git branch --track ${branch##*/} $branch > /dev/null 2>&1
734
        done
735
        git branch
736
        """
737
        webclient_branches = _run(cmd, False)
738
        webclient_branches = webclient_branches.split()
739

    
740
        # If we have pithos_webclient_branch in config file use this one
741
        # else try to use the same branch as synnefo_branch
742
        # else use an appropriate one.
743
        if pithos_webclient_branch == "":
744
            if synnefo_branch in webclient_branches:
745
                pithos_webclient_branch = synnefo_branch
746
            else:
747
                # If synnefo_branch starts with one of
748
                # 'master', 'hotfix'; use the master branch
749
                if synnefo_branch.startswith('master') or \
750
                        synnefo_branch.startswith('hotfix'):
751
                    pithos_webclient_branch = "master"
752
                # If synnefo_branch starts with one of
753
                # 'develop', 'feature'; use the develop branch
754
                elif synnefo_branch.startswith('develop') or \
755
                        synnefo_branch.startswith('feature'):
756
                    pithos_webclient_branch = "develop"
757
                else:
758
                    self.logger.warning(
759
                        "Cannot determine which pithos-web-client branch to "
760
                        "use based on \"%s\" synnefo branch. "
761
                        "Will use develop." % synnefo_branch)
762
                    pithos_webclient_branch = "develop"
763
        # Checkout branch
764
        self.logger.debug("Checkout \"%s\" branch" %
765
                          _green(pithos_webclient_branch))
766
        cmd = """
767
        cd pithos-web-client
768
        git checkout {0}
769
        """.format(pithos_webclient_branch)
770
        _run(cmd, False)
771

    
772
    def _git_clone(self, repo):
773
        """Clone repo to remote server
774

775
        Currently clonning from code.grnet.gr can fail unexpectedly.
776
        So retry!!
777

778
        """
779
        cloned = False
780
        for i in range(1, 11):
781
            try:
782
                _run("git clone %s" % repo, False)
783
                cloned = True
784
                break
785
            except BaseException:
786
                self.logger.warning("Clonning failed.. retrying %s/10" % i)
787
        if not cloned:
788
            self.logger.error("Can not clone repo.")
789
            sys.exit(1)
790

    
791
    @_check_fabric
792
    def build_packages(self):
793
        """Build packages needed by Synnefo software"""
794
        self.logger.info("Install development packages")
795
        cmd = """
796
        apt-get update
797
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
798
                python-dev python-all python-pip ant --yes --force-yes
799
        pip install -U devflow
800
        """
801
        _run(cmd, False)
802

    
803
        # Patch pydist bug
804
        if self.config.get('Global', 'patch_pydist') == "True":
805
            self.logger.debug("Patch pydist.py module")
806
            cmd = r"""
807
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
808
                /usr/share/python/debpython/pydist.py
809
            """
810
            _run(cmd, False)
811

812
        # Build synnefo packages
813
        self.build_synnefo()
814
        # Build pithos-web-client packages
815
        self.build_pithos_webclient()
816

817
    @_check_fabric
818
    def build_synnefo(self):
819
        """Build Synnefo packages"""
820
        self.logger.info("Build Synnefo packages..")
821

822
        cmd = """
823
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
824
        """
825
        with fabric.cd("synnefo"):
826
            _run(cmd, True)
827

828
        # Install snf-deploy package
829
        self.logger.debug("Install snf-deploy package")
830
        cmd = """
831
        dpkg -i snf-deploy*.deb
832
        apt-get -f install --yes --force-yes
833
        """
834
        with fabric.cd("synnefo_build-area"):
835
            with fabric.settings(warn_only=True):
836
                _run(cmd, True)
837

838
        # Setup synnefo packages for snf-deploy
839
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
840
        cmd = """
841
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
842
        """
843
        _run(cmd, False)
844

845
    @_check_fabric
846
    def build_pithos_webclient(self):
847
        """Build pithos-web-client packages"""
848
        self.logger.info("Build pithos-web-client packages..")
849

850
        cmd = """
851
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
852
        """
853
        with fabric.cd("pithos-web-client"):
854
            _run(cmd, True)
855

856
        # Setup pithos-web-client packages for snf-deploy
857
        self.logger.debug("Copy webclient debs to snf-deploy packages dir")
858
        cmd = """
859
        cp ~/webclient_build-area/*.deb /var/lib/snf-deploy/packages/
860
        """
861
        _run(cmd, False)
862

863
    @_check_fabric
864
    def build_documentation(self):
865
        """Build Synnefo documentation"""
866
        self.logger.info("Build Synnefo documentation..")
867
        _run("pip install -U Sphinx", False)
868
        with fabric.cd("synnefo"):
869
            _run("devflow-update-version; "
870
                 "./ci/make_docs.sh synnefo_documentation", False)
871

872
    def fetch_documentation(self, dest=None):
873
        """Fetch Synnefo documentation"""
874
        self.logger.info("Fetch Synnefo documentation..")
875
        if dest is None:
876
            dest = "synnefo_documentation"
877
        dest = os.path.abspath(dest)
878
        if not os.path.exists(dest):
879
            os.makedirs(dest)
880
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
881
        self.logger.info("Downloaded documentation to %s" %
882
                         _green(dest))
883

884
    @_check_fabric
885
    def deploy_synnefo(self, schema=None):
886
        """Deploy Synnefo using snf-deploy"""
887
        self.logger.info("Deploy Synnefo..")
888
        if schema is None:
889
            schema = self.config.get('Global', 'schema')
890
        self.logger.debug("Will use \"%s\" schema" % _green(schema))
891

892
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
893
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
894
            raise ValueError("Unknown schema: %s" % schema)
895

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

899
        self.logger.debug("Change password in nodes.conf file")
900
        cmd = """
901
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
902
        """.format(fabric.env.password)
903
        _run(cmd, False)
904

905
        self.logger.debug("Run snf-deploy")
906
        cmd = """
907
        snf-deploy keygen --force
908
        snf-deploy --disable-colors --autoconf all
909
        """
910
        _run(cmd, True)
911

912
    @_check_fabric
913
    def unit_test(self):
914
        """Run Synnefo unit test suite"""
915
        self.logger.info("Run Synnefo unit test suite")
916
        component = self.config.get('Unit Tests', 'component')
917

918
        self.logger.debug("Install needed packages")
919
        cmd = """
920
        pip install -U mock
921
        pip install -U factory_boy
922
        pip install -U nose
923
        """
924
        _run(cmd, False)
925

926
        self.logger.debug("Upload tests.sh file")
927
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
928
        _put(unit_tests_file, ".")
929

930
        self.logger.debug("Run unit tests")
931
        cmd = """
932
        bash tests.sh {0}
933
        """.format(component)
934
        _run(cmd, True)
935

936
    @_check_fabric
937
    def run_burnin(self):
938
        """Run burnin functional test suite"""
939
        self.logger.info("Run Burnin functional test suite")
940
        cmd = """
941
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
942
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
943
        images_user=$(kamaki image list -l | grep owner | \
944
                      cut -d':' -f2 | tr -d ' ')
945
        snf-burnin --auth-url=$auth_url --token=$token {0}
946
        BurninExitStatus=$?
947
        exit $BurninExitStatus
948
        """.format(self.config.get('Burnin', 'cmd_options'))
949
        _run(cmd, True)
950

951
    @_check_fabric
952
    def fetch_compressed(self, src, dest=None):
953
        """Create a tarball and fetch it locally"""
954
        self.logger.debug("Creating tarball of %s" % src)
955
        basename = os.path.basename(src)
956
        tar_file = basename + ".tgz"
957
        cmd = "tar czf %s %s" % (tar_file, src)
958
        _run(cmd, False)
959
        if not os.path.exists(dest):
960
            os.makedirs(dest)
961

962
        tmp_dir = tempfile.mkdtemp()
963
        fabric.get(tar_file, tmp_dir)
964

965
        dest_file = os.path.join(tmp_dir, tar_file)
966
        self._check_hash_sum(dest_file, tar_file)
967
        self.logger.debug("Untar packages file %s" % dest_file)
968
        cmd = """
969
        cd %s
970
        tar xzf %s
971
        cp -r %s/* %s
972
        rm -r %s
973
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
974
        os.system(cmd)
975
        self.logger.info("Downloaded %s to %s" %
976
                         (src, _green(dest)))
977

978
    @_check_fabric
979
    def fetch_packages(self, dest=None):
980
        """Fetch Synnefo packages"""
981
        if dest is None:
982
            dest = self.config.get('Global', 'pkgs_dir')
983
        dest = os.path.abspath(os.path.expanduser(dest))
984
        if not os.path.exists(dest):
985
            os.makedirs(dest)
986
        self.fetch_compressed("synnefo_build-area", dest)
987
        self.fetch_compressed("webclient_build-area", dest)
988
        self.logger.info("Downloaded debian packages to %s" %
989
                         _green(dest))
990

991
    def x2go_plugin(self, dest=None):
992
        """Produce an html page which will use the x2goplugin
993

    
994
        Arguments:
995
          dest  -- The file where to save the page (String)
996

    
997
        """
998
        output_str = """
999
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
1000
        <html>
1001
        <head>
1002
        <title>X2Go SynnefoCI Service</title>
1003
        </head>
1004
        <body onload="checkPlugin()">
1005
        <div id="x2goplugin">
1006
            <object
1007
                src="location"
1008
                type="application/x2go"
1009
                name="x2goplugin"
1010
                palette="background"
1011
                height="100%"
1012
                hspace="0"
1013
                vspace="0"
1014
                width="100%"
1015
                x2goconfig="
1016
                    session=X2Go-SynnefoCI-Session
1017
                    server={0}
1018
                    user={1}
1019
                    sshport={2}
1020
                    published=true
1021
                    autologin=true
1022
                ">
1023
            </object>
1024
        </div>
1025
        </body>
1026
        </html>
1027
        """.format(self.read_temp_config('server_ip'),
1028
                   self.read_temp_config('server_user'),
1029
                   self.read_temp_config('server_port'))
1030
        if dest is None:
1031
            dest = self.config.get('Global', 'x2go_plugin_file')
1032

1033
        self.logger.info("Writting x2go plugin html file to %s" % dest)
1034
        fid = open(dest, 'w')
1035
        fid.write(output_str)
1036
        fid.close()
1037

1038

1039
def parse_typed_option(option, value):
1040
    """Parsed typed options (flavors and images)"""
1041
    try:
1042
        [type_, val] = value.strip().split(':')
1043
        if type_ not in ["id", "name"]:
1044
            raise ValueError
1045
        return type_, val
1046
    except ValueError:
1047
        msg = "Invalid %s format. Must be [id|name]:.+" % option
1048
        raise ValueError(msg)
1049

1050

1051
def get_endpoint_url(endpoints, endpoint_type):
1052
    """Get the publicURL for the specified endpoint"""
1053

1054
    service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
1055
    return service_catalog[0]['endpoints'][0]['publicURL']
1056