Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ a7d32e21

History | View | Annotate | Download (37.4 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
# Is our terminal a colorful one?
26
USE_COLORS = True
27
# UUID of owner of system images
28
DEFAULT_SYSTEM_IMAGES_UUID = [
29
    "25ecced9-bf53-4145-91ee-cf47377e9fb2",  # production (okeanos.grnet.gr)
30
    "04cbe33f-29b7-4ef1-94fb-015929e5fc06",  # testing (okeanos.io)
31
]
32

    
33

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

    
43

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

    
49

    
50
def _red(msg):
51
    """Red color"""
52
    ret = "\x1b[31m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
53
    return ret
54

    
55

    
56
def _yellow(msg):
57
    """Yellow color"""
58
    ret = "\x1b[33m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
59
    return ret
60

    
61

    
62
def _green(msg):
63
    """Green color"""
64
    ret = "\x1b[32m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg)
65
    return ret
66

    
67

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

    
78

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

    
89

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

    
106

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

    
114

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

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

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

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

    
136
        self.logger.addHandler(handler1)
137
        self.logger.addHandler(handler2)
138

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

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

    
151
        # Read temporary_config file
152
        self.temp_config_file = \
153
            os.path.expanduser(self.config.get('Global', 'temporary_config'))
154
        self.temp_config = ConfigParser()
155
        self.temp_config.optionxform = str
156
        self.temp_config.read(self.temp_config_file)
157
        self.build_id = build_id
158
        if build_id is not None:
159
            self.logger.info("Will use \"%s\" as build id" %
160
                             _green(self.build_id))
161

    
162
        # Set kamaki cloud
163
        if cloud is not None:
164
            self.kamaki_cloud = cloud
165
        elif self.config.has_option("Deployment", "kamaki_cloud"):
166
            kamaki_cloud = self.config.get("Deployment", "kamaki_cloud")
167
            if kamaki_cloud == "":
168
                self.kamaki_cloud = None
169
        else:
170
            self.kamaki_cloud = None
171

    
172
        # Initialize variables
173
        self.fabric_installed = False
174
        self.kamaki_installed = False
175
        self.cyclades_client = None
176
        self.compute_client = None
177
        self.image_client = None
178
        self.astakos_client = None
179

    
180
    def setup_kamaki(self):
181
        """Initialize kamaki
182

183
        Setup cyclades_client, image_client and compute_client
184
        """
185

    
186
        config = kamaki_config.Config()
187
        if self.kamaki_cloud is None:
188
            try:
189
                self.kamaki_cloud = config.get("global", "default_cloud")
190
            except AttributeError:
191
                # Compatibility with kamaki version <=0.10
192
                self.kamaki_cloud = config.get_global("default_cloud")
193

    
194
        self.logger.info("Setup kamaki client, using cloud '%s'.." %
195
                         self.kamaki_cloud)
196
        auth_url = config.get_cloud(self.kamaki_cloud, "url")
197
        self.logger.debug("Authentication URL is %s" % _green(auth_url))
198
        token = config.get_cloud(self.kamaki_cloud, "token")
199
        #self.logger.debug("Token is %s" % _green(token))
200

    
201
        self.astakos_client = AstakosClient(auth_url, token)
202

    
203
        cyclades_url = \
204
            self.astakos_client.get_service_endpoints('compute')['publicURL']
205
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
206
        self.cyclades_client = CycladesClient(cyclades_url, token)
207
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
208

    
209
        image_url = \
210
            self.astakos_client.get_service_endpoints('image')['publicURL']
211
        self.logger.debug("Images API url is %s" % _green(image_url))
212
        self.image_client = ImageClient(cyclades_url, token)
213
        self.image_client.CONNECTION_RETRY_LIMIT = 2
214

    
215
        compute_url = \
216
            self.astakos_client.get_service_endpoints('compute')['publicURL']
217
        self.logger.debug("Compute API url is %s" % _green(compute_url))
218
        self.compute_client = ComputeClient(compute_url, token)
219
        self.compute_client.CONNECTION_RETRY_LIMIT = 2
220

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

    
245
    @_check_kamaki
246
    def destroy_server(self, wait=True):
247
        """Destroy slave server"""
248
        server_id = int(self.read_temp_config('server_id'))
249
        self.logger.info("Destoying server with id %s " % server_id)
250
        self.cyclades_client.delete_server(server_id)
251
        if wait:
252
            self._wait_transition(server_id, "ACTIVE", "DELETED")
253

    
254
    @_check_kamaki
255
    def create_server(self, image=None, flavor=None, ssh_keys=None):
256
        """Create slave server"""
257
        self.logger.info("Create a new server..")
258

    
259
        # Find a build_id to use
260
        self._create_new_build_id()
261

    
262
        # Find an image to use
263
        image_id = self._find_image(image)
264
        # Find a flavor to use
265
        flavor_id = self._find_flavor(flavor)
266

    
267
        # Create Server
268
        server_name = self.config.get("Deployment", "server_name")
269
        server = self.cyclades_client.create_server(
270
            "%s(BID: %s)" % (server_name, self.build_id),
271
            flavor_id,
272
            image_id)
273
        server_id = server['id']
274
        self.write_temp_config('server_id', server_id)
275
        self.logger.debug("Server got id %s" % _green(server_id))
276
        server_user = server['metadata']['users']
277
        self.write_temp_config('server_user', server_user)
278
        self.logger.debug("Server's admin user is %s" % _green(server_user))
279
        server_passwd = server['adminPass']
280
        self.write_temp_config('server_passwd', server_passwd)
281

    
282
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
283
        self._get_server_ip_and_port(server)
284
        self._copy_ssh_keys(ssh_keys)
285

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

    
302
        # Setup apt, download packages
303
        self.logger.debug("Setup apt. Install x2goserver and firefox")
304
        cmd = """
305
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
306
        echo 'precedence ::ffff:0:0/96  100' >> /etc/gai.conf
307
        apt-get update
308
        apt-get install curl --yes --force-yes
309
        echo -e "\n\n{0}" >> /etc/apt/sources.list
310
        # Synnefo repo's key
311
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
312

313
        # X2GO Key
314
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
315
        apt-get install x2go-keyring --yes --force-yes
316
        apt-get update
317
        apt-get install x2goserver x2goserver-xsession \
318
                iceweasel --yes --force-yes
319

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

    
335
    def _find_flavor(self, flavor=None):
336
        """Find a suitable flavor to use
337

338
        Search by name (reg expression) or by id
339
        """
340
        # Get a list of flavors from config file
341
        flavors = self.config.get('Deployment', 'flavors').split(",")
342
        if flavor is not None:
343
            # If we have a flavor_name to use, add it to our list
344
            flavors.insert(0, flavor)
345

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

    
368
            # Check if we found one
369
            if list_flvs:
370
                self.logger.debug("Will use \"%s\" with id \"%s\""
371
                                  % (_green(list_flvs[0]['name']),
372
                                     _green(list_flvs[0]['id'])))
373
                return list_flvs[0]['id']
374

    
375
        self.logger.error("No matching flavor found.. aborting")
376
        sys.exit(1)
377

    
378
    def _find_image(self, image=None):
379
        """Find a suitable image to use
380

381
        In case of search by name, the image has to belong to one
382
        of the `DEFAULT_SYSTEM_IMAGES_UUID' users.
383
        In case of search by id it only has to exist.
384
        """
385
        # Get a list of images from config file
386
        images = self.config.get('Deployment', 'images').split(",")
387
        if image is not None:
388
            # If we have an image from command line, add it to our list
389
            images.insert(0, image)
390

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

    
416
            # Check if we found one
417
            if list_imgs:
418
                self.logger.debug("Will use \"%s\" with id \"%s\""
419
                                  % (_green(list_imgs[0]['name']),
420
                                     _green(list_imgs[0]['id'])))
421
                return list_imgs[0]['id']
422

    
423
        # We didn't found one
424
        self.logger.error("No matching image found.. aborting")
425
        sys.exit(1)
426

    
427
    def _get_server_ip_and_port(self, server):
428
        """Compute server's IPv4 and ssh port number"""
429
        self.logger.info("Get server connection details..")
430
        server_ip = server['attachments'][0]['ipv4']
431
        if (".okeanos.io" in self.cyclades_client.base_url or
432
           ".demo.synnefo.org" in self.cyclades_client.base_url):
433
            tmp1 = int(server_ip.split(".")[2])
434
            tmp2 = int(server_ip.split(".")[3])
435
            server_ip = "gate.okeanos.io"
436
            server_port = 10000 + tmp1 * 256 + tmp2
437
        else:
438
            server_port = 22
439
        self.write_temp_config('server_ip', server_ip)
440
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
441
        self.write_temp_config('server_port', server_port)
442
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
443
        ssh_command = "ssh -p %s %s@%s" \
444
            % (server_port, server['metadata']['users'], server_ip)
445
        self.logger.debug("Access server using \"%s\"" %
446
                          (_green(ssh_command)))
447

    
448
    @_check_fabric
449
    def _copy_ssh_keys(self, ssh_keys):
450
        """Upload/Install ssh keys to server"""
451
        self.logger.debug("Check for authentication keys to use")
452
        if ssh_keys is None:
453
            ssh_keys = self.config.get("Deployment", "ssh_keys")
454

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

    
481
    def _create_new_build_id(self):
482
        """Find a uniq build_id to use"""
483
        with filelocker.lock("%s.lock" % self.temp_config_file,
484
                             filelocker.LOCK_EX):
485
            # Read temp_config again to get any new entries
486
            self.temp_config.read(self.temp_config_file)
487

    
488
            # Find a uniq build_id to use
489
            if self.build_id is None:
490
                ids = self.temp_config.sections()
491
                if ids:
492
                    max_id = int(max(self.temp_config.sections(), key=int))
493
                    self.build_id = max_id + 1
494
                else:
495
                    self.build_id = 1
496
            self.logger.debug("Will use \"%s\" as build id"
497
                              % _green(self.build_id))
498

    
499
            # Create a new section
500
            try:
501
                self.temp_config.add_section(str(self.build_id))
502
            except DuplicateSectionError:
503
                msg = ("Build id \"%s\" already in use. " +
504
                       "Please use a uniq one or cleanup \"%s\" file.\n") \
505
                    % (self.build_id, self.temp_config_file)
506
                self.logger.error(msg)
507
                sys.exit(1)
508
            creation_time = \
509
                time.strftime("%a, %d %b %Y %X", time.localtime())
510
            self.temp_config.set(str(self.build_id),
511
                                 "created", str(creation_time))
512

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

    
517
    def write_temp_config(self, option, value):
518
        """Write changes back to config file"""
519
        # Acquire the lock to write to temp_config_file
520
        with filelocker.lock("%s.lock" % self.temp_config_file,
521
                             filelocker.LOCK_EX):
522

    
523
            # Read temp_config again to get any new entries
524
            self.temp_config.read(self.temp_config_file)
525

    
526
            self.temp_config.set(str(self.build_id), option, str(value))
527
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
528
            self.temp_config.set(str(self.build_id), "modified", curr_time)
529

    
530
            # Write changes back to temp config file
531
            with open(self.temp_config_file, 'wb') as tcf:
532
                self.temp_config.write(tcf)
533

    
534
    def read_temp_config(self, option):
535
        """Read from temporary_config file"""
536
        # If build_id is None use the latest one
537
        if self.build_id is None:
538
            ids = self.temp_config.sections()
539
            if ids:
540
                self.build_id = int(ids[-1])
541
            else:
542
                self.logger.error("No sections in temporary config file")
543
                sys.exit(1)
544
            self.logger.debug("Will use \"%s\" as build id"
545
                              % _green(self.build_id))
546
        # Read specified option
547
        return self.temp_config.get(str(self.build_id), option)
548

    
549
    def setup_fabric(self):
550
        """Setup fabric environment"""
551
        self.logger.info("Setup fabric parameters..")
552
        fabric.env.user = self.read_temp_config('server_user')
553
        fabric.env.host_string = self.read_temp_config('server_ip')
554
        fabric.env.port = int(self.read_temp_config('server_port'))
555
        fabric.env.password = self.read_temp_config('server_passwd')
556
        fabric.env.connection_attempts = 10
557
        fabric.env.shell = "/bin/bash -c"
558
        fabric.env.disable_known_hosts = True
559
        fabric.env.output_prefix = None
560

    
561
    def _check_hash_sum(self, localfile, remotefile):
562
        """Check hash sums of two files"""
563
        self.logger.debug("Check hash sum for local file %s" % localfile)
564
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
565
        self.logger.debug("Local file has sha256 hash %s" % hash1)
566
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
567
        hash2 = _run("sha256sum %s" % remotefile, False)
568
        hash2 = hash2.split(' ')[0]
569
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
570
        if hash1 != hash2:
571
            self.logger.error("Hashes differ.. aborting")
572
            sys.exit(1)
573

    
574
    @_check_fabric
575
    def clone_repo(self, local_repo=False):
576
        """Clone Synnefo repo from slave server"""
577
        self.logger.info("Configure repositories on remote server..")
578
        self.logger.debug("Install/Setup git")
579
        cmd = """
580
        apt-get install git --yes --force-yes
581
        git config --global user.name {0}
582
        git config --global user.email {1}
583
        """.format(self.config.get('Global', 'git_config_name'),
584
                   self.config.get('Global', 'git_config_mail'))
585
        _run(cmd, False)
586

    
587
        # Clone synnefo_repo
588
        synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
589
        # Clone pithos-web-client
590
        self.clone_pithos_webclient_repo(synnefo_branch)
591

    
592
    @_check_fabric
593
    def clone_synnefo_repo(self, local_repo=False):
594
        """Clone Synnefo repo to remote server"""
595
        # Find synnefo_repo and synnefo_branch to use
596
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
597
        synnefo_branch = self.config.get("Global", "synnefo_branch")
598
        if synnefo_branch == "":
599
            synnefo_branch = \
600
                subprocess.Popen(
601
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
602
                    stdout=subprocess.PIPE).communicate()[0].strip()
603
            if synnefo_branch == "HEAD":
604
                synnefo_branch = \
605
                    subprocess.Popen(
606
                        ["git", "rev-parse", "--short", "HEAD"],
607
                        stdout=subprocess.PIPE).communicate()[0].strip()
608
        self.logger.debug("Will use branch \"%s\"" % _green(synnefo_branch))
609

    
610
        if local_repo or synnefo_repo == "":
611
            # Use local_repo
612
            self.logger.debug("Push local repo to server")
613
            # Firstly create the remote repo
614
            _run("git init synnefo", False)
615
            # Then push our local repo over ssh
616
            # We have to pass some arguments to ssh command
617
            # namely to disable host checking.
618
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
619
            os.close(temp_ssh_file_handle)
620
            # XXX: git push doesn't read the password
621
            cmd = """
622
            echo 'exec ssh -o "StrictHostKeyChecking no" \
623
                           -o "UserKnownHostsFile /dev/null" \
624
                           -q "$@"' > {4}
625
            chmod u+x {4}
626
            export GIT_SSH="{4}"
627
            echo "{0}" | git push --quiet --mirror ssh://{1}@{2}:{3}/~/synnefo
628
            rm -f {4}
629
            """.format(fabric.env.password,
630
                       fabric.env.user,
631
                       fabric.env.host_string,
632
                       fabric.env.port,
633
                       temp_ssh_file)
634
            os.system(cmd)
635
        else:
636
            # Clone Synnefo from remote repo
637
            self.logger.debug("Clone synnefo from %s" % synnefo_repo)
638
            self._git_clone(synnefo_repo)
639

    
640
        # Checkout the desired synnefo_branch
641
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
642
        cmd = """
643
        cd synnefo
644
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
645
            git branch --track ${branch##*/} $branch
646
        done
647
        git checkout %s
648
        """ % (synnefo_branch)
649
        _run(cmd, False)
650

    
651
        return synnefo_branch
652

    
653
    @_check_fabric
654
    def clone_pithos_webclient_repo(self, synnefo_branch):
655
        """Clone Pithos WebClient repo to remote server"""
656
        # Find pithos_webclient_repo and pithos_webclient_branch to use
657
        pithos_webclient_repo = \
658
            self.config.get('Global', 'pithos_webclient_repo')
659
        pithos_webclient_branch = \
660
            self.config.get('Global', 'pithos_webclient_branch')
661

    
662
        # Clone pithos-webclient from remote repo
663
        self.logger.debug("Clone pithos-webclient from %s" %
664
                          pithos_webclient_repo)
665
        self._git_clone(pithos_webclient_repo)
666

    
667
        # Track all pithos-webclient branches
668
        cmd = """
669
        cd pithos-web-client
670
        for branch in `git branch -a | grep remotes | grep -v HEAD`; do
671
            git branch --track ${branch##*/} $branch > /dev/null 2>&1
672
        done
673
        git branch
674
        """
675
        webclient_branches = _run(cmd, False)
676
        webclient_branches = webclient_branches.split()
677

    
678
        # If we have pithos_webclient_branch in config file use this one
679
        # else try to use the same branch as synnefo_branch
680
        # else use an appropriate one.
681
        if pithos_webclient_branch == "":
682
            if synnefo_branch in webclient_branches:
683
                pithos_webclient_branch = synnefo_branch
684
            else:
685
                # If synnefo_branch starts with one of
686
                # 'master', 'hotfix'; use the master branch
687
                if synnefo_branch.startswith('master') or \
688
                        synnefo_branch.startswith('hotfix'):
689
                    pithos_webclient_branch = "master"
690
                # If synnefo_branch starts with one of
691
                # 'develop', 'feature'; use the develop branch
692
                elif synnefo_branch.startswith('develop') or \
693
                        synnefo_branch.startswith('feature'):
694
                    pithos_webclient_branch = "develop"
695
                else:
696
                    self.logger.waring(
697
                        "Cannot determine which pithos-web-client branch to "
698
                        "use based on \"%s\" synnefo branch. "
699
                        "Will use develop." % synnefo_branch)
700
                    pithos_webclient_branch = "develop"
701
        # Checkout branch
702
        self.logger.debug("Checkout \"%s\" branch" %
703
                          _green(pithos_webclient_branch))
704
        cmd = """
705
        cd pithos-web-client
706
        git checkout {0}
707
        """.format(pithos_webclient_branch)
708
        _run(cmd, False)
709

    
710
    def _git_clone(self, repo):
711
        """Clone repo to remote server
712

713
        Currently clonning from code.grnet.gr can fail unexpectedly.
714
        So retry!!
715

716
        """
717
        cloned = False
718
        for i in range(1, 11):
719
            try:
720
                _run("git clone %s" % repo, False)
721
                cloned = True
722
                break
723
            except BaseException:
724
                self.logger.warning("Clonning failed.. retrying %s/10" % i)
725
        if not cloned:
726
            self.logger.error("Can not clone repo.")
727
            sys.exit(1)
728

    
729
    @_check_fabric
730
    def build_packages(self):
731
        """Build packages needed by Synnefo software"""
732
        self.logger.info("Install development packages")
733
        cmd = """
734
        apt-get update
735
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
736
                python-dev python-all python-pip ant --yes --force-yes
737
        pip install -U devflow
738
        """
739
        _run(cmd, False)
740

    
741
        # Patch pydist bug
742
        if self.config.get('Global', 'patch_pydist') == "True":
743
            self.logger.debug("Patch pydist.py module")
744
            cmd = r"""
745
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
746
                /usr/share/python/debpython/pydist.py
747
            """
748
            _run(cmd, False)
749

750
        # Build synnefo packages
751
        self.build_synnefo()
752
        # Build pithos-web-client packages
753
        self.build_pithos_webclient()
754

755
    @_check_fabric
756
    def build_synnefo(self):
757
        """Build Synnefo packages"""
758
        self.logger.info("Build Synnefo packages..")
759

760
        cmd = """
761
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
762
        """
763
        with fabric.cd("synnefo"):
764
            _run(cmd, True)
765

766
        # Install snf-deploy package
767
        self.logger.debug("Install snf-deploy package")
768
        cmd = """
769
        dpkg -i snf-deploy*.deb
770
        apt-get -f install --yes --force-yes
771
        """
772
        with fabric.cd("synnefo_build-area"):
773
            with fabric.settings(warn_only=True):
774
                _run(cmd, True)
775

776
        # Setup synnefo packages for snf-deploy
777
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
778
        cmd = """
779
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
780
        """
781
        _run(cmd, False)
782

783
    @_check_fabric
784
    def build_pithos_webclient(self):
785
        """Build pithos-web-client packages"""
786
        self.logger.info("Build pithos-web-client packages..")
787

788
        cmd = """
789
        devflow-autopkg snapshot -b ~/webclient_build-area --no-sign
790
        """
791
        with fabric.cd("pithos-web-client"):
792
            _run(cmd, True)
793

794
        # Setup pithos-web-client packages for snf-deploy
795
        self.logger.debug("Copy webclient debs to snf-deploy packages dir")
796
        cmd = """
797
        cp ~/webclient_build-area/*.deb /var/lib/snf-deploy/packages/
798
        """
799
        _run(cmd, False)
800

801
    @_check_fabric
802
    def build_documentation(self):
803
        """Build Synnefo documentation"""
804
        self.logger.info("Build Synnefo documentation..")
805
        _run("pip install -U Sphinx", False)
806
        with fabric.cd("synnefo"):
807
            _run("devflow-update-version; "
808
                 "./ci/make_docs.sh synnefo_documentation", False)
809

810
    def fetch_documentation(self, dest=None):
811
        """Fetch Synnefo documentation"""
812
        self.logger.info("Fetch Synnefo documentation..")
813
        if dest is None:
814
            dest = "synnefo_documentation"
815
        dest = os.path.abspath(dest)
816
        if not os.path.exists(dest):
817
            os.makedirs(dest)
818
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
819
        self.logger.info("Downloaded documentation to %s" %
820
                         _green(dest))
821

822
    @_check_fabric
823
    def deploy_synnefo(self, schema=None):
824
        """Deploy Synnefo using snf-deploy"""
825
        self.logger.info("Deploy Synnefo..")
826
        if schema is None:
827
            schema = self.config.get('Global', 'schema')
828
        self.logger.debug("Will use \"%s\" schema" % _green(schema))
829

830
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
831
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
832
            raise ValueError("Unknown schema: %s" % schema)
833

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

837
        self.logger.debug("Change password in nodes.conf file")
838
        cmd = """
839
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
840
        """.format(fabric.env.password)
841
        _run(cmd, False)
842

843
        self.logger.debug("Run snf-deploy")
844
        cmd = """
845
        snf-deploy keygen --force
846
        snf-deploy --disable-colors --autoconf all
847
        """
848
        _run(cmd, True)
849

850
    @_check_fabric
851
    def unit_test(self):
852
        """Run Synnefo unit test suite"""
853
        self.logger.info("Run Synnefo unit test suite")
854
        component = self.config.get('Unit Tests', 'component')
855

856
        self.logger.debug("Install needed packages")
857
        cmd = """
858
        pip install -U mock
859
        pip install -U factory_boy
860
        pip install -U nose
861
        """
862
        _run(cmd, False)
863

864
        self.logger.debug("Upload tests.sh file")
865
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
866
        _put(unit_tests_file, ".")
867

868
        self.logger.debug("Run unit tests")
869
        cmd = """
870
        bash tests.sh {0}
871
        """.format(component)
872
        _run(cmd, True)
873

874
    @_check_fabric
875
    def run_burnin(self):
876
        """Run burnin functional test suite"""
877
        self.logger.info("Run Burnin functional test suite")
878
        cmd = """
879
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
880
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
881
        images_user=$(kamaki image list -l | grep owner | \
882
                      cut -d':' -f2 | tr -d ' ')
883
        snf-burnin --auth-url=$auth_url --token=$token \
884
            --force-flavor=2 --image-id=all \
885
            --system-images-user=$images_user \
886
            {0}
887
        BurninExitStatus=$?
888
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
889
        for i in $(ls $log_folder/*/details*); do
890
            echo -e "\\n\\n"
891
            echo -e "***** $i\\n"
892
            cat $i
893
        done
894
        exit $BurninExitStatus
895
        """.format(self.config.get('Burnin', 'cmd_options'))
896
        _run(cmd, True)
897

898
    @_check_fabric
899
    def fetch_compressed(self, src, dest=None):
900
        """Create a tarball and fetch it locally"""
901
        self.logger.debug("Creating tarball of %s" % src)
902
        basename = os.path.basename(src)
903
        tar_file = basename + ".tgz"
904
        cmd = "tar czf %s %s" % (tar_file, src)
905
        _run(cmd, False)
906
        if not os.path.exists(dest):
907
            os.makedirs(dest)
908

909
        tmp_dir = tempfile.mkdtemp()
910
        fabric.get(tar_file, tmp_dir)
911

912
        dest_file = os.path.join(tmp_dir, tar_file)
913
        self._check_hash_sum(dest_file, tar_file)
914
        self.logger.debug("Untar packages file %s" % dest_file)
915
        cmd = """
916
        cd %s
917
        tar xzf %s
918
        cp -r %s/* %s
919
        rm -r %s
920
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
921
        os.system(cmd)
922
        self.logger.info("Downloaded %s to %s" %
923
                         (src, _green(dest)))
924

925
    @_check_fabric
926
    def fetch_packages(self, dest=None):
927
        """Fetch Synnefo packages"""
928
        if dest is None:
929
            dest = self.config.get('Global', 'pkgs_dir')
930
        dest = os.path.abspath(os.path.expanduser(dest))
931
        if not os.path.exists(dest):
932
            os.makedirs(dest)
933
        self.fetch_compressed("synnefo_build-area", dest)
934
        self.fetch_compressed("webclient_build-area", dest)
935
        self.logger.info("Downloaded debian packages to %s" %
936
                         _green(dest))
937

938
    def x2go_plugin(self, dest=None):
939
        """Produce an html page which will use the x2goplugin
940

    
941
        Arguments:
942
          dest  -- The file where to save the page (String)
943

    
944
        """
945
        output_str = """
946
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
947
        <html>
948
        <head>
949
        <title>X2Go SynnefoCI Service</title>
950
        </head>
951
        <body onload="checkPlugin()">
952
        <div id="x2goplugin">
953
            <object
954
                src="location"
955
                type="application/x2go"
956
                name="x2goplugin"
957
                palette="background"
958
                height="100%"
959
                hspace="0"
960
                vspace="0"
961
                width="100%"
962
                x2goconfig="
963
                    session=X2Go-SynnefoCI-Session
964
                    server={0}
965
                    user={1}
966
                    sshport={2}
967
                    published=true
968
                    autologin=true
969
                ">
970
            </object>
971
        </div>
972
        </body>
973
        </html>
974
        """.format(self.read_temp_config('server_ip'),
975
                   self.read_temp_config('server_user'),
976
                   self.read_temp_config('server_port'))
977
        if dest is None:
978
            dest = self.config.get('Global', 'x2go_plugin_file')
979

980
        self.logger.info("Writting x2go plugin html file to %s" % dest)
981
        fid = open(dest, 'w')
982
        fid.write(output_str)
983
        fid.close()
984

985

986
def parse_typed_option(option, value):
987
    """Parsed typed options (flavors and images)"""
988
    try:
989
        [type_, val] = value.strip().split(':')
990
        if type_ not in ["id", "name"]:
991
            raise ValueError
992
        return type_, val
993
    except ValueError:
994
        msg = "Invalid %s format. Must be [id|name]:.+" % option
995
        raise ValueError(msg)
996