Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ c7828946

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
        self._create_new_build_id()
253

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
476
            # Find a uniq build_id to use
477
            if self.build_id is None:
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("Will use \"%s\" as build id"
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