Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 329705c8

History | View | Annotate | Download (33.1 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
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_wheezy.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
            try:
489
                self.temp_config.add_section(str(self.build_id))
490
            except DuplicateSectionError:
491
                msg = ("Build id \"%s\" already in use. " +
492
                       "Please use a uniq one or cleanup \"%s\" file.\n") \
493
                    % (self.build_id, self.temp_config_file)
494
                self.logger.error(msg)
495
                sys.exit(1)
496
            creation_time = \
497
                time.strftime("%a, %d %b %Y %X", time.localtime())
498
            self.temp_config.set(str(self.build_id),
499
                                 "created", str(creation_time))
500

    
501
            # Write changes back to temp config file
502
            with open(self.temp_config_file, 'wb') as tcf:
503
                self.temp_config.write(tcf)
504

    
505
    def write_temp_config(self, option, value):
506
        """Write changes back to config file"""
507
        # Acquire the lock to write to temp_config_file
508
        with filelocker.lock("%s.lock" % self.temp_config_file,
509
                             filelocker.LOCK_EX):
510

    
511
            # Read temp_config again to get any new entries
512
            self.temp_config.read(self.temp_config_file)
513

    
514
            self.temp_config.set(str(self.build_id), option, str(value))
515
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
516
            self.temp_config.set(str(self.build_id), "modified", curr_time)
517

    
518
            # Write changes back to temp config file
519
            with open(self.temp_config_file, 'wb') as tcf:
520
                self.temp_config.write(tcf)
521

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

    
537
    def setup_fabric(self):
538
        """Setup fabric environment"""
539
        self.logger.info("Setup fabric parameters..")
540
        fabric.env.user = self.read_temp_config('server_user')
541
        fabric.env.host_string = self.read_temp_config('server_ip')
542
        fabric.env.port = int(self.read_temp_config('server_port'))
543
        fabric.env.password = self.read_temp_config('server_passwd')
544
        fabric.env.connection_attempts = 10
545
        fabric.env.shell = "/bin/bash -c"
546
        fabric.env.disable_known_hosts = True
547
        fabric.env.output_prefix = None
548

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

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

    
575
        # Find synnefo_repo and synnefo_branch to use
576
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
577
        synnefo_branch = self.config.get("Global", "synnefo_branch")
578
        if synnefo_branch == "":
579
            synnefo_branch = \
580
                subprocess.Popen(
581
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
582
                    stdout=subprocess.PIPE).communicate()[0].strip()
583
            if synnefo_branch == "HEAD":
584
                synnefo_branch = \
585
                    subprocess.Popen(
586
                        ["git", "rev-parse", "--short", "HEAD"],
587
                        stdout=subprocess.PIPE).communicate()[0].strip()
588
        self.logger.info("Will use branch %s" % synnefo_branch)
589

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

    
632
        # Checkout the desired synnefo_branch
633
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
634
        cmd = """
635
        cd synnefo
636
        for branch in `git branch -a | grep remotes | \
637
                       grep -v HEAD | grep -v master`; do
638
            git branch --track ${branch##*/} $branch
639
        done
640
        git checkout %s
641
        """ % (synnefo_branch)
642
        _run(cmd, False)
643

    
644
    @_check_fabric
645
    def build_synnefo(self):
646
        """Build Synnefo packages"""
647
        self.logger.info("Build Synnefo packages..")
648
        self.logger.debug("Install development packages")
649
        cmd = """
650
        apt-get update
651
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
652
                python-dev python-all python-pip --yes --force-yes
653
        pip install -U devflow
654
        """
655
        _run(cmd, False)
656

    
657
        if self.config.get('Global', 'patch_pydist') == "True":
658
            self.logger.debug("Patch pydist.py module")
659
            cmd = r"""
660
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
661
                /usr/share/python/debpython/pydist.py
662
            """
663
            _run(cmd, False)
664

665
        # Build synnefo packages
666
        self.logger.debug("Build synnefo packages")
667
        cmd = """
668
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
669
        """
670
        with fabric.cd("synnefo"):
671
            _run(cmd, True)
672

673
        # Install snf-deploy package
674
        self.logger.debug("Install snf-deploy package")
675
        cmd = """
676
        dpkg -i snf-deploy*.deb
677
        apt-get -f install --yes --force-yes
678
        """
679
        with fabric.cd("synnefo_build-area"):
680
            with fabric.settings(warn_only=True):
681
                _run(cmd, True)
682

683
        # Setup synnefo packages for snf-deploy
684
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
685
        cmd = """
686
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
687
        """
688
        _run(cmd, False)
689

690
    @_check_fabric
691
    def build_documentation(self):
692
        """Build Synnefo documentation"""
693
        self.logger.info("Build Synnefo documentation..")
694
        _run("pip install -U Sphinx", False)
695
        with fabric.cd("synnefo"):
696
            _run("devflow-update-version; "
697
                 "./ci/make_docs.sh synnefo_documentation", False)
698

699
    def fetch_documentation(self, dest=None):
700
        """Fetch Synnefo documentation"""
701
        self.logger.info("Fetch Synnefo documentation..")
702
        if dest is None:
703
            dest = "synnefo_documentation"
704
        dest = os.path.abspath(dest)
705
        if not os.path.exists(dest):
706
            os.makedirs(dest)
707
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
708
        self.logger.info("Downloaded documentation to %s" %
709
                         _green(dest))
710

711
    @_check_fabric
712
    def deploy_synnefo(self, schema=None):
713
        """Deploy Synnefo using snf-deploy"""
714
        self.logger.info("Deploy Synnefo..")
715
        if schema is None:
716
            schema = self.config.get('Global', 'schema')
717
        self.logger.debug("Will use \"%s\" schema" % schema)
718

719
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
720
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
721
            raise ValueError("Unknown schema: %s" % schema)
722

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

726
        self.logger.debug("Change password in nodes.conf file")
727
        cmd = """
728
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
729
        """.format(fabric.env.password)
730
        _run(cmd, False)
731

732
        self.logger.debug("Run snf-deploy")
733
        cmd = """
734
        snf-deploy keygen --force
735
        snf-deploy --disable-colors --autoconf all
736
        """
737
        _run(cmd, True)
738

739
    @_check_fabric
740
    def unit_test(self):
741
        """Run Synnefo unit test suite"""
742
        self.logger.info("Run Synnefo unit test suite")
743
        component = self.config.get('Unit Tests', 'component')
744

745
        self.logger.debug("Install needed packages")
746
        cmd = """
747
        pip install -U mock
748
        pip install -U factory_boy
749
        """
750
        _run(cmd, False)
751

752
        self.logger.debug("Upload tests.sh file")
753
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
754
        _put(unit_tests_file, ".")
755

756
        self.logger.debug("Run unit tests")
757
        cmd = """
758
        bash tests.sh {0}
759
        """.format(component)
760
        _run(cmd, True)
761

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

786
    @_check_fabric
787
    def fetch_compressed(self, src, dest=None):
788
        """Create a tarball and fetch it locally"""
789
        self.logger.debug("Creating tarball of %s" % src)
790
        basename = os.path.basename(src)
791
        tar_file = basename + ".tgz"
792
        cmd = "tar czf %s %s" % (tar_file, src)
793
        _run(cmd, False)
794
        if not os.path.exists(dest):
795
            os.makedirs(dest)
796

797
        tmp_dir = tempfile.mkdtemp()
798
        fabric.get(tar_file, tmp_dir)
799

800
        dest_file = os.path.join(tmp_dir, tar_file)
801
        self._check_hash_sum(dest_file, tar_file)
802
        self.logger.debug("Untar packages file %s" % dest_file)
803
        cmd = """
804
        cd %s
805
        tar xzf %s
806
        cp -r %s/* %s
807
        rm -r %s
808
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
809
        os.system(cmd)
810
        self.logger.info("Downloaded %s to %s" %
811
                         (src, _green(dest)))
812

813
    @_check_fabric
814
    def fetch_packages(self, dest=None):
815
        """Fetch Synnefo packages"""
816
        if dest is None:
817
            dest = self.config.get('Global', 'pkgs_dir')
818
        dest = os.path.abspath(os.path.expanduser(dest))
819
        if not os.path.exists(dest):
820
            os.makedirs(dest)
821
        self.fetch_compressed("synnefo_build-area", dest)
822
        self.logger.info("Downloaded debian packages to %s" %
823
                         _green(dest))
824

825
    def x2go_plugin(self, dest=None):
826
        """Produce an html page which will use the x2goplugin
827

    
828
        Arguments:
829
          dest  -- The file where to save the page (String)
830

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

867
        self.logger.info("Writting x2go plugin html file to %s" % dest)
868
        fid = open(dest, 'w')
869
        fid.write(output_str)
870
        fid.close()
871

872

873
def parse_typed_option(option, value):
874
    """Parsed typed options (flavors and images)"""
875
    try:
876
        [type_, val] = value.strip().split(':')
877
        if type_ not in ["id", "name"]:
878
            raise ValueError
879
        return type_, val
880
    except ValueError:
881
        msg = "Invalid %s format. Must be [id|name]:.+" % option
882
        raise ValueError(msg)
883