Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ ac7b865d

History | View | Annotate | Download (40.9 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
    # pylint: disable= no-self-use
266
    @_check_fabric
267
    def shell_connect(self):
268
        """Open shell to remote server"""
269
        fabric.open_shell("export TERM=xterm")
270

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

    
289
    def _create_port(self, floating_ip):
290
        """Create a new port for our floating IP"""
291
        net_id = floating_ip['floating_network_id']
292
        self.logger.debug("Creating a new port to network with id %s", net_id)
293
        fixed_ips = [{'ip_address': floating_ip['floating_ip_address']}]
294
        port = self.network_client.create_port(
295
            net_id, device_id=None, fixed_ips=fixed_ips)
296
        return port
297

    
298
    @_check_kamaki
299
    # Too many local variables. pylint: disable-msg=R0914
300
    def create_server(self, image=None, flavor=None, ssh_keys=None,
301
                      server_name=None):
302
        """Create slave server"""
303
        self.logger.info("Create a new server..")
304

    
305
        # Find a build_id to use
306
        self._create_new_build_id()
307

    
308
        # Find an image to use
309
        image_id = self._find_image(image)
310
        # Find a flavor to use
311
        flavor_id = self._find_flavor(flavor)
312

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

    
337
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
338
        self._get_server_ip_and_port(server, private_networks)
339
        self._copy_ssh_keys(ssh_keys)
340

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

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

368
        # X2GO Key
369
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
370
        apt-get install x2go-keyring --yes --force-yes
371
        apt-get update
372
        apt-get install x2goserver x2goserver-xsession \
373
                iceweasel --yes --force-yes
374

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

    
390
    def _find_flavor(self, flavor=None):
391
        """Find a suitable flavor to use
392

393
        Search by name (reg expression) or by id
394
        """
395
        # Get a list of flavors from config file
396
        flavors = self.config.get('Deployment', 'flavors').split(",")
397
        if flavor is not None:
398
            # If we have a flavor_name to use, add it to our list
399
            flavors.insert(0, flavor)
400

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

    
423
            # Check if we found one
424
            if list_flvs:
425
                self.logger.debug("Will use \"%s\" with id \"%s\""
426
                                  % (_green(list_flvs[0]['name']),
427
                                     _green(list_flvs[0]['id'])))
428
                return list_flvs[0]['id']
429

    
430
        self.logger.error("No matching flavor found.. aborting")
431
        sys.exit(1)
432

    
433
    def _find_image(self, image=None):
434
        """Find a suitable image to use
435

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

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

    
471
            # Check if we found one
472
            if list_imgs:
473
                self.logger.debug("Will use \"%s\" with id \"%s\""
474
                                  % (_green(list_imgs[0]['name']),
475
                                     _green(list_imgs[0]['id'])))
476
                return list_imgs[0]['id']
477

    
478
        # We didn't found one
479
        self.logger.error("No matching image found.. aborting")
480
        sys.exit(1)
481

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

    
499
        # Check if config has ssh_port option and if so, use that port.
500
        if self.config.has_option("Deployment", "ssh_port"):
501
            server_port = self.config.get("Deployment", "ssh_port")
502
        else:
503
            if (".okeanos.io" in self.cyclades_client.base_url or
504
            ".demo.synnefo.org" in self.cyclades_client.base_url):
505
                tmp1 = int(server_ip.split(".")[2])
506
                tmp2 = int(server_ip.split(".")[3])
507
                server_ip = "gate.okeanos.io"
508
                server_port = 10000 + tmp1 * 256 + tmp2
509
            else:
510
                server_port = 22
511

    
512
        self.write_temp_config('server_ip', server_ip)
513
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
514
        self.write_temp_config('server_port', server_port)
515
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
516
        ssh_command = "ssh -p %s %s@%s" \
517
            % (server_port, server['metadata']['users'], server_ip)
518
        self.logger.debug("Access server using \"%s\"" %
519
                          (_green(ssh_command)))
520

    
521
    @_check_fabric
522
    def _copy_ssh_keys(self, ssh_keys):
523
        """Upload/Install ssh keys to server"""
524
        self.logger.debug("Check for authentication keys to use")
525
        if ssh_keys is None:
526
            ssh_keys = self.config.get("Deployment", "ssh_keys")
527

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

    
554
    def _create_new_build_id(self):
555
        """Find a uniq build_id to use"""
556
        with filelocker.lock("%s.lock" % self.temp_config_file,
557
                             filelocker.LOCK_EX):
558
            # Read temp_config again to get any new entries
559
            self.temp_config.read(self.temp_config_file)
560

    
561
            # Find a uniq build_id to use
562
            if self.build_id is None:
563
                ids = self.temp_config.sections()
564
                if ids:
565
                    max_id = int(max(self.temp_config.sections(), key=int))
566
                    self.build_id = max_id + 1
567
                else:
568
                    self.build_id = 1
569
            self.logger.debug("Will use \"%s\" as build id"
570
                              % _green(self.build_id))
571

    
572
            # Create a new section
573
            try:
574
                self.temp_config.add_section(str(self.build_id))
575
            except DuplicateSectionError:
576
                msg = ("Build id \"%s\" already in use. " +
577
                       "Please use a uniq one or cleanup \"%s\" file.\n") \
578
                    % (self.build_id, self.temp_config_file)
579
                self.logger.error(msg)
580
                sys.exit(1)
581
            creation_time = \
582
                time.strftime("%a, %d %b %Y %X", time.localtime())
583
            self.temp_config.set(str(self.build_id),
584
                                 "created", str(creation_time))
585

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

    
590
    def write_temp_config(self, option, value):
591
        """Write changes back to config file"""
592
        # Acquire the lock to write to temp_config_file
593
        with filelocker.lock("%s.lock" % self.temp_config_file,
594
                             filelocker.LOCK_EX):
595

    
596
            # Read temp_config again to get any new entries
597
            self.temp_config.read(self.temp_config_file)
598

    
599
            self.temp_config.set(str(self.build_id), option, str(value))
600
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
601
            self.temp_config.set(str(self.build_id), "modified", curr_time)
602

    
603
            # Write changes back to temp config file
604
            with open(self.temp_config_file, 'wb') as tcf:
605
                self.temp_config.write(tcf)
606

    
607
    def read_temp_config(self, option):
608
        """Read from temporary_config file"""
609
        # If build_id is None use the latest one
610
        if self.build_id is None:
611
            ids = self.temp_config.sections()
612
            if ids:
613
                self.build_id = int(ids[-1])
614
            else:
615
                self.logger.error("No sections in temporary config file")
616
                sys.exit(1)
617
            self.logger.debug("Will use \"%s\" as build id"
618
                              % _green(self.build_id))
619
        # Read specified option
620
        return self.temp_config.get(str(self.build_id), option)
621

    
622
    def setup_fabric(self):
623
        """Setup fabric environment"""
624
        self.logger.info("Setup fabric parameters..")
625
        fabric.env.user = self.read_temp_config('server_user')
626
        fabric.env.host_string = self.read_temp_config('server_ip')
627
        fabric.env.port = int(self.read_temp_config('server_port'))
628
        fabric.env.password = self.read_temp_config('server_passwd')
629
        fabric.env.connection_attempts = 10
630
        fabric.env.shell = "/bin/bash -c"
631
        fabric.env.disable_known_hosts = True
632
        fabric.env.output_prefix = None
633

    
634
    def _check_hash_sum(self, localfile, remotefile):
635
        """Check hash sums of two files"""
636
        self.logger.debug("Check hash sum for local file %s" % localfile)
637
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
638
        self.logger.debug("Local file has sha256 hash %s" % hash1)
639
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
640
        hash2 = _run("sha256sum %s" % remotefile, False)
641
        hash2 = hash2.split(' ')[0]
642
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
643
        if hash1 != hash2:
644
            self.logger.error("Hashes differ.. aborting")
645
            sys.exit(1)
646

    
647
    @_check_fabric
648
    def clone_repo(self, local_repo=False):
649
        """Clone Synnefo repo from slave server"""
650
        self.logger.info("Configure repositories on remote server..")
651
        self.logger.debug("Install/Setup git")
652
        cmd = """
653
        apt-get install git --yes --force-yes
654
        git config --global user.name {0}
655
        git config --global user.email {1}
656
        """.format(self.config.get('Global', 'git_config_name'),
657
                   self.config.get('Global', 'git_config_mail'))
658
        _run(cmd, False)
659

    
660
        # Clone synnefo_repo
661
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
662
        # Clone pithos-web-client
663
        self.clone_pithos_webclient_repo(synnefo_branch)
664

    
665
    @_check_fabric
666
    def clone_synnefo_repo(self, local_repo=False):
667
        """Clone Synnefo repo to remote server"""
668
        # Find synnefo_repo and synnefo_branch to use
669
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
670
        synnefo_branch = self.config.get("Global", "synnefo_branch")
671
        if synnefo_branch == "":
672
            synnefo_branch = \
673
                subprocess.Popen(
674
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
675
                    stdout=subprocess.PIPE).communicate()[0].strip()
676
            if synnefo_branch == "HEAD":
677
                synnefo_branch = \
678
                    subprocess.Popen(
679
                        ["git", "rev-parse", "--short", "HEAD"],
680
                        stdout=subprocess.PIPE).communicate()[0].strip()
681
        self.logger.debug("Will use branch \"%s\"" % _green(synnefo_branch))
682

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

    
713
        # Checkout the desired synnefo_branch
714
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
715
        cmd = """
716
        cd synnefo
717
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
718
            git branch --track ${branch##*/} $branch
719
        done
720
        git checkout %s
721
        """ % (synnefo_branch)
722
        _run(cmd, False)
723

    
724
        return synnefo_branch
725

    
726
    @_check_fabric
727
    def clone_pithos_webclient_repo(self, synnefo_branch):
728
        """Clone Pithos WebClient repo to remote server"""
729
        # Find pithos_webclient_repo and pithos_webclient_branch to use
730
        pithos_webclient_repo = \
731
            self.config.get('Global', 'pithos_webclient_repo')
732
        pithos_webclient_branch = \
733
            self.config.get('Global', 'pithos_webclient_branch')
734

    
735
        # Clone pithos-webclient from remote repo
736
        self.logger.debug("Clone pithos-webclient from %s" %
737
                          pithos_webclient_repo)
738
        self._git_clone(pithos_webclient_repo)
739

    
740
        # Track all pithos-webclient branches
741
        cmd = """
742
        cd pithos-web-client
743
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
744
            git branch --track ${branch##*/} $branch > /dev/null 2>&1
745
        done
746
        git --no-pager branch --no-color
747
        """
748
        webclient_branches = _run(cmd, False)
749
        webclient_branches = webclient_branches.split()
750

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

    
783
    def _git_clone(self, repo):
784
        """Clone repo to remote server
785

786
        Currently clonning from code.grnet.gr can fail unexpectedly.
787
        So retry!!
788

789
        """
790
        cloned = False
791
        for i in range(1, 11):
792
            try:
793
                _run("git clone %s" % repo, False)
794
                cloned = True
795
                break
796
            except BaseException:
797
                self.logger.warning("Clonning failed.. retrying %s/10" % i)
798
        if not cloned:
799
            self.logger.error("Can not clone repo.")
800
            sys.exit(1)
801

    
802
    @_check_fabric
803
    def build_packages(self):
804
        """Build packages needed by Synnefo software"""
805
        self.logger.info("Install development packages")
806
        cmd = """
807
        apt-get update
808
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
809
                python-dev python-all python-pip ant --yes --force-yes
810
        pip install -U devflow
811
        """
812
        _run(cmd, False)
813

    
814
        # Patch pydist bug
815
        if self.config.get('Global', 'patch_pydist') == "True":
816
            self.logger.debug("Patch pydist.py module")
817
            cmd = r"""
818
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
819
                /usr/share/python/debpython/pydist.py
820
            """
821
            _run(cmd, False)
822

823
        # Build synnefo packages
824
        self.build_synnefo()
825
        # Build pithos-web-client packages
826
        self.build_pithos_webclient()
827

828
    @_check_fabric
829
    def build_synnefo(self):
830
        """Build Synnefo packages"""
831
        self.logger.info("Build Synnefo packages..")
832

833
        cmd = """
834
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
835
        """
836
        with fabric.cd("synnefo"):
837
            _run(cmd, True)
838

839
        # Install snf-deploy package
840
        self.logger.debug("Install snf-deploy package")
841
        cmd = """
842
        dpkg -i snf-deploy*.deb
843
        apt-get -f install --yes --force-yes
844
        """
845
        with fabric.cd("synnefo_build-area"):
846
            with fabric.settings(warn_only=True):
847
                _run(cmd, True)
848

849
        # Setup synnefo packages for snf-deploy
850
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
851
        cmd = """
852
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
853
        """
854
        _run(cmd, False)
855

856
    @_check_fabric
857
    def build_pithos_webclient(self):
858
        """Build pithos-web-client packages"""
859
        self.logger.info("Build pithos-web-client packages..")
860

861
        cmd = """
862
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
863
        """
864
        with fabric.cd("pithos-web-client"):
865
            _run(cmd, True)
866

867
        # Setup pithos-web-client packages for snf-deploy
868
        self.logger.debug("Copy webclient debs to snf-deploy packages dir")
869
        cmd = """
870
        cp ~/webclient_build-area/*.deb /var/lib/snf-deploy/packages/
871
        """
872
        _run(cmd, False)
873

874
    @_check_fabric
875
    def build_documentation(self):
876
        """Build Synnefo documentation"""
877
        self.logger.info("Build Synnefo documentation..")
878
        _run("pip install -U Sphinx", False)
879
        with fabric.cd("synnefo"):
880
            _run("devflow-update-version; "
881
                 "./ci/make_docs.sh synnefo_documentation", False)
882

883
    def fetch_documentation(self, dest=None):
884
        """Fetch Synnefo documentation"""
885
        self.logger.info("Fetch Synnefo documentation..")
886
        if dest is None:
887
            dest = "synnefo_documentation"
888
        dest = os.path.abspath(dest)
889
        if not os.path.exists(dest):
890
            os.makedirs(dest)
891
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
892
        self.logger.info("Downloaded documentation to %s" %
893
                         _green(dest))
894

895
    @_check_fabric
896
    def deploy_synnefo(self, schema=None):
897
        """Deploy Synnefo using snf-deploy"""
898
        self.logger.info("Deploy Synnefo..")
899
        if schema is None:
900
            schema = self.config.get('Global', 'schema')
901
        self.logger.debug("Will use \"%s\" schema" % _green(schema))
902

903
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
904
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
905
            raise ValueError("Unknown schema: %s" % schema)
906

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

910
        self.logger.debug("Change password in nodes.conf file")
911
        cmd = """
912
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
913
        sed -i 's/12345/{0}/' /etc/snf-deploy/nodes.conf
914
        """.format(fabric.env.password)
915
        _run(cmd, False)
916

917
        self.logger.debug("Run snf-deploy")
918
        cmd = """
919
        snf-deploy keygen --force
920
        snf-deploy --disable-colors --autoconf all
921
        """
922
        _run(cmd, True)
923

924
    @_check_fabric
925
    def unit_test(self):
926
        """Run Synnefo unit test suite"""
927
        self.logger.info("Run Synnefo unit test suite")
928
        component = self.config.get('Unit Tests', 'component')
929

930
        self.logger.debug("Install needed packages")
931
        cmd = """
932
        pip install -U mock
933
        pip install -U factory_boy
934
        pip install -U nose
935
        """
936
        _run(cmd, False)
937

938
        self.logger.debug("Upload tests.sh file")
939
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
940
        _put(unit_tests_file, ".")
941

942
        self.logger.debug("Run unit tests")
943
        cmd = """
944
        bash tests.sh {0}
945
        """.format(component)
946
        _run(cmd, True)
947

948
    @_check_fabric
949
    def run_burnin(self):
950
        """Run burnin functional test suite"""
951
        self.logger.info("Run Burnin functional test suite")
952
        cmd = """
953
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
954
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
955
        images_user=$(kamaki image list -l | grep owner | \
956
                      cut -d':' -f2 | tr -d ' ')
957
        snf-burnin --auth-url=$auth_url --token=$token {0}
958
        BurninExitStatus=$?
959
        exit $BurninExitStatus
960
        """.format(self.config.get('Burnin', 'cmd_options'))
961
        _run(cmd, True)
962

963
    @_check_fabric
964
    def fetch_compressed(self, src, dest=None):
965
        """Create a tarball and fetch it locally"""
966
        self.logger.debug("Creating tarball of %s" % src)
967
        basename = os.path.basename(src)
968
        tar_file = basename + ".tgz"
969
        cmd = "tar czf %s %s" % (tar_file, src)
970
        _run(cmd, False)
971
        if not os.path.exists(dest):
972
            os.makedirs(dest)
973

974
        tmp_dir = tempfile.mkdtemp()
975
        fabric.get(tar_file, tmp_dir)
976

977
        dest_file = os.path.join(tmp_dir, tar_file)
978
        self._check_hash_sum(dest_file, tar_file)
979
        self.logger.debug("Untar packages file %s" % dest_file)
980
        cmd = """
981
        cd %s
982
        tar xzf %s
983
        cp -r %s/* %s
984
        rm -r %s
985
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
986
        os.system(cmd)
987
        self.logger.info("Downloaded %s to %s" %
988
                         (src, _green(dest)))
989

990
    @_check_fabric
991
    def fetch_packages(self, dest=None):
992
        """Fetch Synnefo packages"""
993
        if dest is None:
994
            dest = self.config.get('Global', 'pkgs_dir')
995
        dest = os.path.abspath(os.path.expanduser(dest))
996
        if not os.path.exists(dest):
997
            os.makedirs(dest)
998
        self.fetch_compressed("synnefo_build-area", dest)
999
        self.fetch_compressed("webclient_build-area", dest)
1000
        self.logger.info("Downloaded debian packages to %s" %
1001
                         _green(dest))
1002

1003
    def x2go_plugin(self, dest=None):
1004
        """Produce an html page which will use the x2goplugin
1005

    
1006
        Arguments:
1007
          dest  -- The file where to save the page (String)
1008

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

1045
        self.logger.info("Writting x2go plugin html file to %s" % dest)
1046
        fid = open(dest, 'w')
1047
        fid.write(output_str)
1048
        fid.close()
1049

1050

1051
def parse_typed_option(option, value):
1052
    """Parsed typed options (flavors and images)"""
1053
    try:
1054
        [type_, val] = value.strip().split(':')
1055
        if type_ not in ["id", "name"]:
1056
            raise ValueError
1057
        return type_, val
1058
    except ValueError:
1059
        msg = "Invalid %s format. Must be [id|name]:.+" % option
1060
        raise ValueError(msg)
1061

1062

1063
def get_endpoint_url(endpoints, endpoint_type):
1064
    """Get the publicURL for the specified endpoint"""
1065

1066
    service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
1067
    return service_catalog[0]['endpoints'][0]['publicURL']
1068