Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 2cbdb63f

History | View | Annotate | Download (32.7 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
16

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

    
24
DEFAULT_CONFIG_FILE = "ci_squeeze.conf"
25
# UUID of owner of system images
26
DEFAULT_SYSTEM_IMAGES_UUID = [
27
    "25ecced9-bf53-4145-91ee-cf47377e9fb2",  # production (okeanos.grnet.gr)
28
    "04cbe33f-29b7-4ef1-94fb-015929e5fc06",  # testing (okeanos.io)
29
]
30

    
31

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

    
41

    
42
def _put(local, remote):
43
    """Run fabric put command without output"""
44
    with fabric.quiet():
45
        fabric.put(local, remote)
46

    
47

    
48
def _red(msg):
49
    """Red color"""
50
    #return "\x1b[31m" + str(msg) + "\x1b[0m"
51
    return str(msg)
52

    
53

    
54
def _yellow(msg):
55
    """Yellow color"""
56
    #return "\x1b[33m" + str(msg) + "\x1b[0m"
57
    return str(msg)
58

    
59

    
60
def _green(msg):
61
    """Green color"""
62
    #return "\x1b[32m" + str(msg) + "\x1b[0m"
63
    return str(msg)
64

    
65

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

    
76

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

    
87

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

    
104

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

    
112

    
113
# Too many instance attributes. pylint: disable-msg=R0902
114
class SynnefoCI(object):
115
    """SynnefoCI python class"""
116

    
117
    def __init__(self, config_file=None, build_id=None, cloud=None):
118
        """ Initialize SynnefoCI python class
119

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

    
126
        handler1 = logging.StreamHandler(sys.stdout)
127
        handler1.setLevel(logging.DEBUG)
128
        handler1.addFilter(_InfoFilter())
129
        handler1.setFormatter(_MyFormatter())
130
        handler2 = logging.StreamHandler(sys.stderr)
131
        handler2.setLevel(logging.WARNING)
132
        handler2.setFormatter(_MyFormatter())
133

    
134
        self.logger.addHandler(handler1)
135
        self.logger.addHandler(handler2)
136

    
137
        # Get our local dir
138
        self.ci_dir = os.path.dirname(os.path.abspath(__file__))
139
        self.repo_dir = os.path.dirname(self.ci_dir)
140

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

    
149
        # Read temporary_config file
150
        self.temp_config_file = \
151
            os.path.expanduser(self.config.get('Global', 'temporary_config'))
152
        self.temp_config = ConfigParser()
153
        self.temp_config.optionxform = str
154
        self.temp_config.read(self.temp_config_file)
155
        self.build_id = build_id
156
        self.logger.info("Will use \"%s\" as build id" % _green(self.build_id))
157

    
158
        # Set kamaki cloud
159
        if cloud is not None:
160
            self.kamaki_cloud = cloud
161
        elif self.config.has_option("Deployment", "kamaki_cloud"):
162
            kamaki_cloud = self.config.get("Deployment", "kamaki_cloud")
163
            if kamaki_cloud == "":
164
                self.kamaki_cloud = None
165
        else:
166
            self.kamaki_cloud = None
167

    
168
        # Initialize variables
169
        self.fabric_installed = False
170
        self.kamaki_installed = False
171
        self.cyclades_client = None
172
        self.compute_client = None
173
        self.image_client = None
174
        self.astakos_client = None
175

    
176
    def setup_kamaki(self):
177
        """Initialize kamaki
178

179
        Setup cyclades_client, image_client and compute_client
180
        """
181

    
182
        config = kamaki_config.Config()
183
        if self.kamaki_cloud is None:
184
            self.kamaki_cloud = config.get_global("default_cloud")
185

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

    
193
        self.astakos_client = AstakosClient(auth_url, token)
194

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

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

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

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

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

    
246
    @_check_kamaki
247
    def create_server(self, image=None, flavor=None, ssh_keys=None):
248
        """Create slave server"""
249
        self.logger.info("Create a new server..")
250

    
251
        # Find a build_id to use
252
        if self.build_id is None:
253
            self._create_new_build_id()
254

    
255
        # Find an image to use
256
        image_id = self._find_image(image)
257
        # Find a flavor to use
258
        flavor_id = self._find_flavor(flavor)
259

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

    
275
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
276
        self._get_server_ip_and_port(server)
277
        self._copy_ssh_keys(ssh_keys)
278

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

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

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

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

    
327
    def _find_flavor(self, flavor=None):
328
        """Find a suitable flavor to use
329

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

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

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

    
366
        self.logger.error("No matching flavor found.. aborting")
367
        sys.exit(1)
368

    
369
    def _find_image(self, image=None):
370
        """Find a suitable image to use
371

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

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

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

    
413
        # We didn't found one
414
        self.logger.error("No matching image found.. aborting")
415
        sys.exit(1)
416

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

    
437
    @_check_fabric
438
    def _copy_ssh_keys(self, ssh_keys):
439
        """Upload/Install ssh keys to server"""
440
        self.logger.debug("Check for authentication keys to use")
441
        if ssh_keys is None:
442
            ssh_keys = self.config.get("Deployment", "ssh_keys")
443

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

    
470
    def _create_new_build_id(self):
471
        """Find a uniq build_id to use"""
472
        with filelocker.lock("%s.lock" % self.temp_config_file,
473
                             filelocker.LOCK_EX):
474
            # Read temp_config again to get any new entries
475
            self.temp_config.read(self.temp_config_file)
476

    
477
            # Find a uniq build_id to use
478
            ids = self.temp_config.sections()
479
            if ids:
480
                max_id = int(max(self.temp_config.sections(), key=int))
481
                self.build_id = max_id + 1
482
            else:
483
                self.build_id = 1
484
            self.logger.debug("New build id \"%s\" was created"
485
                              % _green(self.build_id))
486

    
487
            # Create a new section
488
            self.temp_config.add_section(str(self.build_id))
489
            creation_time = \
490
                time.strftime("%a, %d %b %Y %X", time.localtime())
491
            self.temp_config.set(str(self.build_id),
492
                                 "created", str(creation_time))
493

    
494
            # Write changes back to temp config file
495
            with open(self.temp_config_file, 'wb') as tcf:
496
                self.temp_config.write(tcf)
497

    
498
    def write_temp_config(self, option, value):
499
        """Write changes back to config file"""
500
        # Acquire the lock to write to temp_config_file
501
        with filelocker.lock("%s.lock" % self.temp_config_file,
502
                             filelocker.LOCK_EX):
503

    
504
            # Read temp_config again to get any new entries
505
            self.temp_config.read(self.temp_config_file)
506

    
507
            self.temp_config.set(str(self.build_id), option, str(value))
508
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
509
            self.temp_config.set(str(self.build_id), "modified", curr_time)
510

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

    
515
    def read_temp_config(self, option):
516
        """Read from temporary_config file"""
517
        # If build_id is None use the latest one
518
        if self.build_id is None:
519
            ids = self.temp_config.sections()
520
            if ids:
521
                self.build_id = int(ids[-1])
522
            else:
523
                self.logger.error("No sections in temporary config file")
524
                sys.exit(1)
525
            self.logger.debug("Will use \"%s\" as build id"
526
                              % _green(self.build_id))
527
        # Read specified option
528
        return self.temp_config.get(str(self.build_id), option)
529

    
530
    def setup_fabric(self):
531
        """Setup fabric environment"""
532
        self.logger.info("Setup fabric parameters..")
533
        fabric.env.user = self.read_temp_config('server_user')
534
        fabric.env.host_string = self.read_temp_config('server_ip')
535
        fabric.env.port = int(self.read_temp_config('server_port'))
536
        fabric.env.password = self.read_temp_config('server_passwd')
537
        fabric.env.connection_attempts = 10
538
        fabric.env.shell = "/bin/bash -c"
539
        fabric.env.disable_known_hosts = True
540
        fabric.env.output_prefix = None
541

    
542
    def _check_hash_sum(self, localfile, remotefile):
543
        """Check hash sums of two files"""
544
        self.logger.debug("Check hash sum for local file %s" % localfile)
545
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
546
        self.logger.debug("Local file has sha256 hash %s" % hash1)
547
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
548
        hash2 = _run("sha256sum %s" % remotefile, False)
549
        hash2 = hash2.split(' ')[0]
550
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
551
        if hash1 != hash2:
552
            self.logger.error("Hashes differ.. aborting")
553
            sys.exit(-1)
554

    
555
    @_check_fabric
556
    def clone_repo(self, local_repo=False):
557
        """Clone Synnefo repo from slave server"""
558
        self.logger.info("Configure repositories on remote server..")
559
        self.logger.debug("Install/Setup git")
560
        cmd = """
561
        apt-get install git --yes --force-yes
562
        git config --global user.name {0}
563
        git config --global user.email {1}
564
        """.format(self.config.get('Global', 'git_config_name'),
565
                   self.config.get('Global', 'git_config_mail'))
566
        _run(cmd, False)
567

    
568
        # Find synnefo_repo and synnefo_branch to use
569
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
570
        synnefo_branch = self.config.get("Global", "synnefo_branch")
571
        if synnefo_branch == "":
572
            synnefo_branch = \
573
                subprocess.Popen(
574
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
575
                    stdout=subprocess.PIPE).communicate()[0].strip()
576
            if synnefo_branch == "HEAD":
577
                synnefo_branch = \
578
                    subprocess.Popen(
579
                        ["git", "rev-parse", "--short", "HEAD"],
580
                        stdout=subprocess.PIPE).communicate()[0].strip()
581
        self.logger.info("Will use branch %s" % synnefo_branch)
582

    
583
        if local_repo or synnefo_branch == "":
584
            # Use local_repo
585
            self.logger.debug("Push local repo to server")
586
            # Firstly create the remote repo
587
            _run("git init synnefo", False)
588
            # Then push our local repo over ssh
589
            # We have to pass some arguments to ssh command
590
            # namely to disable host checking.
591
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
592
            os.close(temp_ssh_file_handle)
593
            # XXX: git push doesn't read the password
594
            cmd = """
595
            echo 'exec ssh -o "StrictHostKeyChecking no" \
596
                           -o "UserKnownHostsFile /dev/null" \
597
                           -q "$@"' > {4}
598
            chmod u+x {4}
599
            export GIT_SSH="{4}"
600
            echo "{0}" | git push --mirror ssh://{1}@{2}:{3}/~/synnefo
601
            rm -f {4}
602
            """.format(fabric.env.password,
603
                       fabric.env.user,
604
                       fabric.env.host_string,
605
                       fabric.env.port,
606
                       temp_ssh_file)
607
            os.system(cmd)
608
        else:
609
            # Clone Synnefo from remote repo
610
            # Currently clonning synnefo can fail unexpectedly
611
            cloned = False
612
            for i in range(10):
613
                self.logger.debug("Clone synnefo from %s" % synnefo_repo)
614
                try:
615
                    _run("git clone %s synnefo" % synnefo_repo, False)
616
                    cloned = True
617
                    break
618
                except BaseException:
619
                    self.logger.warning(
620
                        "Clonning synnefo failed.. retrying %s" % i)
621
            if not cloned:
622
                self.logger.error("Can not clone Synnefo repo.")
623
                sys.exit(-1)
624

    
625
        # Checkout the desired synnefo_branch
626
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
627
        cmd = """
628
        cd synnefo
629
        for branch in `git branch -a | grep remotes | \
630
                       grep -v HEAD | grep -v master`; do
631
            git branch --track ${branch##*/} $branch
632
        done
633
        git checkout %s
634
        """ % (synnefo_branch)
635
        _run(cmd, False)
636

    
637
    @_check_fabric
638
    def build_synnefo(self):
639
        """Build Synnefo packages"""
640
        self.logger.info("Build Synnefo packages..")
641
        self.logger.debug("Install development packages")
642
        cmd = """
643
        apt-get update
644
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
645
                python-dev python-all python-pip --yes --force-yes
646
        pip install devflow
647
        """
648
        _run(cmd, False)
649

    
650
        if self.config.get('Global', 'patch_pydist') == "True":
651
            self.logger.debug("Patch pydist.py module")
652
            cmd = r"""
653
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
654
                /usr/share/python/debpython/pydist.py
655
            """
656
            _run(cmd, False)
657

658
        # Build synnefo packages
659
        self.logger.debug("Build synnefo packages")
660
        cmd = """
661
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
662
        """
663
        with fabric.cd("synnefo"):
664
            _run(cmd, True)
665

666
        # Install snf-deploy package
667
        self.logger.debug("Install snf-deploy package")
668
        cmd = """
669
        dpkg -i snf-deploy*.deb
670
        apt-get -f install --yes --force-yes
671
        """
672
        with fabric.cd("synnefo_build-area"):
673
            with fabric.settings(warn_only=True):
674
                _run(cmd, True)
675

676
        # Setup synnefo packages for snf-deploy
677
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
678
        cmd = """
679
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
680
        """
681
        _run(cmd, False)
682

683
    @_check_fabric
684
    def build_documentation(self):
685
        """Build Synnefo documentation"""
686
        self.logger.info("Build Synnefo documentation..")
687
        _run("pip install -U Sphinx", False)
688
        with fabric.cd("synnefo"):
689
            _run("devflow-update-version; "
690
                 "./ci/make_docs.sh synnefo_documentation", False)
691

692
    def fetch_documentation(self, dest=None):
693
        """Fetch Synnefo documentation"""
694
        self.logger.info("Fetch Synnefo documentation..")
695
        if dest is None:
696
            dest = "synnefo_documentation"
697
        dest = os.path.abspath(dest)
698
        if not os.path.exists(dest):
699
            os.makedirs(dest)
700
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
701
        self.logger.info("Downloaded documentation to %s" %
702
                         _green(dest))
703

704
    @_check_fabric
705
    def deploy_synnefo(self, schema=None):
706
        """Deploy Synnefo using snf-deploy"""
707
        self.logger.info("Deploy Synnefo..")
708
        if schema is None:
709
            schema = self.config.get('Global', 'schema')
710
        self.logger.debug("Will use \"%s\" schema" % schema)
711

712
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
713
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
714
            raise ValueError("Unknown schema: %s" % schema)
715

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

719
        self.logger.debug("Change password in nodes.conf file")
720
        cmd = """
721
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
722
        """.format(fabric.env.password)
723
        _run(cmd, False)
724

725
        self.logger.debug("Run snf-deploy")
726
        cmd = """
727
        snf-deploy keygen --force
728
        snf-deploy --disable-colors --autoconf all
729
        """
730
        _run(cmd, True)
731

732
    @_check_fabric
733
    def unit_test(self):
734
        """Run Synnefo unit test suite"""
735
        self.logger.info("Run Synnefo unit test suite")
736
        component = self.config.get('Unit Tests', 'component')
737

738
        self.logger.debug("Install needed packages")
739
        cmd = """
740
        pip install mock
741
        pip install factory_boy
742
        """
743
        _run(cmd, False)
744

745
        self.logger.debug("Upload tests.sh file")
746
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
747
        _put(unit_tests_file, ".")
748

749
        self.logger.debug("Run unit tests")
750
        cmd = """
751
        bash tests.sh {0}
752
        """.format(component)
753
        _run(cmd, True)
754

755
    @_check_fabric
756
    def run_burnin(self):
757
        """Run burnin functional test suite"""
758
        self.logger.info("Run Burnin functional test suite")
759
        cmd = """
760
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
761
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
762
        images_user=$(kamaki image list -l | grep owner | \
763
                      cut -d':' -f2 | tr -d ' ')
764
        snf-burnin --auth-url=$auth_url --token=$token \
765
            --force-flavor=2 --image-id=all \
766
            --system-images-user=$images_user \
767
            {0}
768
        BurninExitStatus=$?
769
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
770
        for i in $(ls $log_folder/*/details*); do
771
            echo -e "\\n\\n"
772
            echo -e "***** $i\\n"
773
            cat $i
774
        done
775
        exit $BurninExitStatus
776
        """.format(self.config.get('Burnin', 'cmd_options'))
777
        _run(cmd, True)
778

779
    @_check_fabric
780
    def fetch_compressed(self, src, dest=None):
781
        """Create a tarball and fetch it locally"""
782
        self.logger.debug("Creating tarball of %s" % src)
783
        basename = os.path.basename(src)
784
        tar_file = basename + ".tgz"
785
        cmd = "tar czf %s %s" % (tar_file, src)
786
        _run(cmd, False)
787
        if not os.path.exists(dest):
788
            os.makedirs(dest)
789

790
        tmp_dir = tempfile.mkdtemp()
791
        fabric.get(tar_file, tmp_dir)
792

793
        dest_file = os.path.join(tmp_dir, tar_file)
794
        self._check_hash_sum(dest_file, tar_file)
795
        self.logger.debug("Untar packages file %s" % dest_file)
796
        cmd = """
797
        cd %s
798
        tar xzf %s
799
        cp -r %s/* %s
800
        rm -r %s
801
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
802
        os.system(cmd)
803
        self.logger.info("Downloaded %s to %s" %
804
                         (src, _green(dest)))
805

806
    @_check_fabric
807
    def fetch_packages(self, dest=None):
808
        """Fetch Synnefo packages"""
809
        if dest is None:
810
            dest = self.config.get('Global', 'pkgs_dir')
811
        dest = os.path.abspath(os.path.expanduser(dest))
812
        if not os.path.exists(dest):
813
            os.makedirs(dest)
814
        self.fetch_compressed("synnefo_build-area", dest)
815
        self.logger.info("Downloaded debian packages to %s" %
816
                         _green(dest))
817

818
    def x2go_plugin(self, dest=None):
819
        """Produce an html page which will use the x2goplugin
820

    
821
        Arguments:
822
          dest  -- The file where to save the page (String)
823

    
824
        """
825
        output_str = """
826
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
827
        <html>
828
        <head>
829
        <title>X2Go SynnefoCI Service</title>
830
        </head>
831
        <body onload="checkPlugin()">
832
        <div id="x2goplugin">
833
            <object
834
                src="location"
835
                type="application/x2go"
836
                name="x2goplugin"
837
                palette="background"
838
                height="100%"
839
                hspace="0"
840
                vspace="0"
841
                width="100%"
842
                x2goconfig="
843
                    session=X2Go-SynnefoCI-Session
844
                    server={0}
845
                    user={1}
846
                    sshport={2}
847
                    published=true
848
                    autologin=true
849
                ">
850
            </object>
851
        </div>
852
        </body>
853
        </html>
854
        """.format(self.read_temp_config('server_ip'),
855
                   self.read_temp_config('server_user'),
856
                   self.read_temp_config('server_port'))
857
        if dest is None:
858
            dest = self.config.get('Global', 'x2go_plugin_file')
859

860
        self.logger.info("Writting x2go plugin html file to %s" % dest)
861
        fid = open(dest, 'w')
862
        fid.write(output_str)
863
        fid.close()
864

865

866
def parse_typed_option(option, value):
867
    """Parsed typed options (flavors and images)"""
868
    try:
869
        [type_, val] = value.strip().split(':')
870
        if type_ not in ["id", "name"]:
871
            raise ValueError
872
        return type_, val
873
    except ValueError:
874
        msg = "Invalid %s format. Must be [id|name]:.+" % option
875
        raise ValueError(msg)
876