Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 2eda9c61

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
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 = "  %(message)s"
96
        elif record.levelno == logging.INFO:
97
            self._fmt = "%(message)s"
98
        elif record.levelno == logging.WARNING:
99
            self._fmt = _yellow("[W] %(message)s")
100
        elif record.levelno == logging.ERROR:
101
            self._fmt = _red("[E] %(message)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.network_client = None
177
        self.compute_client = None
178
        self.image_client = None
179
        self.astakos_client = None
180

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

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

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

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

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

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

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

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

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

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

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

    
264
    def _create_floating_ip(self):
265
        """Create a new floating ip"""
266
        networks = self.network_client.list_networks(detail=True)
267
        pub_net = [n for n in networks
268
                   if n['SNF:floating_ip_pool'] and n['public']]
269
        pub_net = pub_net[0]
270
        fip = self.network_client.create_floatingip(pub_net['id'])
271
        self.logger.debug("Floating IP %s with id %s created",
272
                          fip['floating_ip_address'], fip['id'])
273
        return fip
274

    
275
    def _create_port(self, floating_ip):
276
        """Create a new port for our floating IP"""
277
        net_id = floating_ip['floating_network_id']
278
        self.logger.debug("Creating a new port to network with id %s", net_id)
279
        fixed_ips = [{'ip_address': floating_ip['floating_ip_address']}]
280
        port = self.network_client.create_port(
281
            net_id, device_id=None, fixed_ips=fixed_ips)
282
        return port
283

    
284
    @_check_kamaki
285
    # Too many local variables. pylint: disable-msg=R0914
286
    def create_server(self, image=None, flavor=None, ssh_keys=None,
287
                      server_name=None):
288
        """Create slave server"""
289
        self.logger.info("Create a new server..")
290

    
291
        # Find a build_id to use
292
        self._create_new_build_id()
293

    
294
        # Find an image to use
295
        image_id = self._find_image(image)
296
        # Find a flavor to use
297
        flavor_id = self._find_flavor(flavor)
298

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

    
323
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
324
        self._get_server_ip_and_port(server, private_networks)
325
        self._copy_ssh_keys(ssh_keys)
326

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

    
343
        # Setup apt, download packages
344
        self.logger.debug("Setup apt")
345
        cmd = """
346
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
347
        echo 'precedence ::ffff:0:0/96  100' >> /etc/gai.conf
348
        apt-get update
349
        apt-get install -q=2 curl --yes --force-yes
350
        echo -e "\n\n{0}" >> /etc/apt/sources.list
351
        # Synnefo repo's key
352
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
353
        """.format(self.config.get('Global', 'apt_repo'))
354
        _run(cmd, False)
355

    
356
        cmd = """
357
        # X2GO Key
358
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
359
        apt-get install x2go-keyring --yes --force-yes
360
        apt-get update
361
        apt-get install x2goserver x2goserver-xsession \
362
                iceweasel --yes --force-yes
363

364
        # xterm published application
365
        echo '[Desktop Entry]' > /usr/share/applications/xterm.desktop
366
        echo 'Name=XTerm' >> /usr/share/applications/xterm.desktop
367
        echo 'Comment=standard terminal emulator for the X window system' >> \
368
            /usr/share/applications/xterm.desktop
369
        echo 'Exec=xterm' >> /usr/share/applications/xterm.desktop
370
        echo 'Terminal=false' >> /usr/share/applications/xterm.desktop
371
        echo 'Type=Application' >> /usr/share/applications/xterm.desktop
372
        echo 'Encoding=UTF-8' >> /usr/share/applications/xterm.desktop
373
        echo 'Icon=xterm-color_48x48' >> /usr/share/applications/xterm.desktop
374
        echo 'Categories=System;TerminalEmulator;' >> \
375
                /usr/share/applications/xterm.desktop"""
376
        if self.config.get("Global", "setup_x2go") == "True":
377
            self.logger.debug("Install x2goserver and firefox")
378
            _run(cmd, False)
379

    
380
    def _find_flavor(self, flavor=None):
381
        """Find a suitable flavor to use
382

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

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

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

    
420
        self.logger.error("No matching flavor found.. aborting")
421
        sys.exit(1)
422

    
423
    def _find_image(self, image=None):
424
        """Find a suitable image to use
425

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

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

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

    
468
        # We didn't found one
469
        self.logger.error("No matching image found.. aborting")
470
        sys.exit(1)
471

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

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

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

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

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

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

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

    
571
            # Write changes back to temp config file
572
            with open(self.temp_config_file, 'wb') as tcf:
573
                self.temp_config.write(tcf)
574

    
575
    def write_temp_config(self, option, value):
576
        """Write changes back to config file"""
577
        # Acquire the lock to write to temp_config_file
578
        with filelocker.lock("%s.lock" % self.temp_config_file,
579
                             filelocker.LOCK_EX):
580

    
581
            # Read temp_config again to get any new entries
582
            self.temp_config.read(self.temp_config_file)
583

    
584
            self.temp_config.set(str(self.build_id), option, str(value))
585
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
586
            self.temp_config.set(str(self.build_id), "modified", curr_time)
587

    
588
            # Write changes back to temp config file
589
            with open(self.temp_config_file, 'wb') as tcf:
590
                self.temp_config.write(tcf)
591

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

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

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

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

    
645
        # Clone synnefo_repo
646
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
647

    
648
        if self.config.get("Global", "build_pithos_webclient") == "True":
649
            # Clone pithos-web-client
650
            self.clone_pithos_webclient_repo(synnefo_branch)
651

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

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

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

    
711
        return synnefo_branch
712

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

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

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

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

    
770
    def _git_clone(self, repo):
771
        """Clone repo to remote server
772

773
        Currently clonning from code.grnet.gr can fail unexpectedly.
774
        So retry!!
775

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

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

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

810
        # Build synnefo packages
811
        self.build_synnefo()
812
        # Build pithos-web-client packages
813
        if self.config.get("Global", "build_pithos_webclient") == "True":
814
            self.build_pithos_webclient()
815

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

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

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

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

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

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

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

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

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

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

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

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

898
        self.logger.debug("Change password in nodes.conf file")
899
        cmd = """
900
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
901
        sed -i 's/12345/{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