Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 1185ff2e

History | View | Annotate | Download (40.2 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. Install x2goserver and firefox")
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 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

354
        # X2GO Key
355
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
356
        apt-get install x2go-keyring --yes --force-yes
357
        apt-get update
358
        apt-get install x2goserver x2goserver-xsession \
359
                iceweasel --yes --force-yes
360

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

    
376
    def _find_flavor(self, flavor=None):
377
        """Find a suitable flavor to use
378

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

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

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

    
416
        self.logger.error("No matching flavor found.. aborting")
417
        sys.exit(1)
418

    
419
    def _find_image(self, image=None):
420
        """Find a suitable image to use
421

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

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

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

    
464
        # We didn't found one
465
        self.logger.error("No matching image found.. aborting")
466
        sys.exit(1)
467

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

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

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

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

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

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

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

    
567
            # Write changes back to temp config file
568
            with open(self.temp_config_file, 'wb') as tcf:
569
                self.temp_config.write(tcf)
570

    
571
    def write_temp_config(self, option, value):
572
        """Write changes back to config file"""
573
        # Acquire the lock to write to temp_config_file
574
        with filelocker.lock("%s.lock" % self.temp_config_file,
575
                             filelocker.LOCK_EX):
576

    
577
            # Read temp_config again to get any new entries
578
            self.temp_config.read(self.temp_config_file)
579

    
580
            self.temp_config.set(str(self.build_id), option, str(value))
581
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
582
            self.temp_config.set(str(self.build_id), "modified", curr_time)
583

    
584
            # Write changes back to temp config file
585
            with open(self.temp_config_file, 'wb') as tcf:
586
                self.temp_config.write(tcf)
587

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

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

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

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

    
641
        # Clone synnefo_repo
642
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
643

    
644
        if self.config.get("Global", "build_pithos_webclient") == "True":
645
            # Clone pithos-web-client
646
            self.clone_pithos_webclient_repo(synnefo_branch)
647

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

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

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

    
707
        return synnefo_branch
708

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

    
718
        # Clone pithos-webclient from remote repo
719
        self.logger.debug("Clone pithos-webclient from %s" %
720
                          pithos_webclient_repo)
721
        self._git_clone(pithos_webclient_repo)
722

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

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

    
766
    def _git_clone(self, repo):
767
        """Clone repo to remote server
768

769
        Currently clonning from code.grnet.gr can fail unexpectedly.
770
        So retry!!
771

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

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

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

806
        # Build synnefo packages
807
        self.build_synnefo()
808
        # Build pithos-web-client packages
809
        if self.config.get("Global", "build_pithos_webclient") == "True":
810
            self.build_pithos_webclient()
811

812
    @_check_fabric
813
    def build_synnefo(self):
814
        """Build Synnefo packages"""
815
        self.logger.info("Build Synnefo packages..")
816

817
        cmd = """
818
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
819
        """
820
        with fabric.cd("synnefo"):
821
            _run(cmd, True)
822

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

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

840
    @_check_fabric
841
    def build_pithos_webclient(self):
842
        """Build pithos-web-client packages"""
843
        self.logger.info("Build pithos-web-client packages..")
844

845
        cmd = """
846
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
847
        """
848
        with fabric.cd("pithos-web-client"):
849
            _run(cmd, True)
850

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

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

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

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

887
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
888
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
889
            raise ValueError("Unknown schema: %s" % schema)
890

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

894
        self.logger.debug("Change password in nodes.conf file")
895
        cmd = """
896
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
897
        sed -i 's/12345/{0}/' /etc/snf-deploy/nodes.conf
898
        """.format(fabric.env.password)
899
        _run(cmd, False)
900

901
        self.logger.debug("Run snf-deploy")
902
        cmd = """
903
        snf-deploy keygen --force
904
        snf-deploy --disable-colors --autoconf all
905
        """
906
        _run(cmd, True)
907

908
    @_check_fabric
909
    def unit_test(self):
910
        """Run Synnefo unit test suite"""
911
        self.logger.info("Run Synnefo unit test suite")
912
        component = self.config.get('Unit Tests', 'component')
913

914
        self.logger.debug("Install needed packages")
915
        cmd = """
916
        pip install -U mock
917
        pip install -U factory_boy
918
        pip install -U nose
919
        """
920
        _run(cmd, False)
921

922
        self.logger.debug("Upload tests.sh file")
923
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
924
        _put(unit_tests_file, ".")
925

926
        self.logger.debug("Run unit tests")
927
        cmd = """
928
        bash tests.sh {0}
929
        """.format(component)
930
        _run(cmd, True)
931

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

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

958
        tmp_dir = tempfile.mkdtemp()
959
        fabric.get(tar_file, tmp_dir)
960

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

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

987
    def x2go_plugin(self, dest=None):
988
        """Produce an html page which will use the x2goplugin
989

    
990
        Arguments:
991
          dest  -- The file where to save the page (String)
992

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

1029
        self.logger.info("Writting x2go plugin html file to %s" % dest)
1030
        fid = open(dest, 'w')
1031
        fid.write(output_str)
1032
        fid.close()
1033

1034

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

1046

1047
def get_endpoint_url(endpoints, endpoint_type):
1048
    """Get the publicURL for the specified endpoint"""
1049

1050
    service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
1051
    return service_catalog[0]['endpoints'][0]['publicURL']
1052