Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 4b61ee63

History | View | Annotate | Download (29.2 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 = "new_config"
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 = DEFAULT_CONFIG_FILE
144
        if not os.path.isabs(config_file):
145
            config_file = os.path.join(self.ci_dir, config_file)
146
        self.config = ConfigParser()
147
        self.config.optionxform = str
148
        self.config.read(config_file)
149

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

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

    
169
        # Initialize variables
170
        self.fabric_installed = False
171
        self.kamaki_installed = False
172
        self.cyclades_client = None
173
        self.compute_client = None
174
        self.image_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
        astakos_client = AstakosClient(auth_url, token)
194

    
195
        cyclades_url = \
196
            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
            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
            astakos_client.get_service_endpoints('compute')['publicURL']
209
        self.logger.debug("Compute API url is %s" % _green(compute_url))
210
        self.compute_client = ComputeClient(compute_url, token)
211
        self.compute_client.CONNECTION_RETRY_LIMIT = 2
212

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

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

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

    
251
        # Find a build_id to use
252
        if self.build_id is None:
253
            # If build_id is given use this, else ..
254
            # Find a uniq build_id to use
255
            ids = self.temp_config.sections()
256
            if ids:
257
                max_id = int(max(self.temp_config.sections(), key=int))
258
                self.build_id = max_id + 1
259
            else:
260
                self.build_id = 1
261
        self.logger.debug("New build id \"%s\" was created"
262
                          % _green(self.build_id))
263

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

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

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

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

    
304
        # Setup apt, download packages
305
        self.logger.debug("Setup apt. Install x2goserver and firefox")
306
        cmd = """
307
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
308
        apt-get update
309
        apt-get install curl --yes
310
        echo -e "\n\n{0}" >> /etc/apt/sources.list
311
        # Synnefo repo's key
312
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
313
        # X2GO Key
314
        apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
315
        apt-get install x2go-keyring --yes
316
        apt-get update
317
        apt-get install x2goserver x2goserver-xsession iceweasel --yes
318
        """.format(self.config.get('Global', 'apt_repo'))
319
        _run(cmd, False)
320

    
321
    def _find_flavor(self, flavor=None):
322
        """Find a suitable flavor to use
323

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

    
332
        list_flavors = self.compute_client.list_flavors()
333
        for flv in flavors:
334
            [flv_type, flv_value] = flv.strip().split(':')
335
            if flv_type == "name":
336
                # Filter flavors by name
337
                self.logger.debug(
338
                    "Trying to find a flavor with name \"%s\"" % flv_value)
339
                list_flvs = \
340
                    [f for f in list_flavors
341
                     if re.search(flv_value, f['name'], flags=re.I) is not None]
342
            elif flv_type == "id":
343
                # Filter flavors by id
344
                self.logger.debug(
345
                    "Trying to find a flavor with id \"%s\"" % flv_value)
346
                list_flvs = \
347
                    [f for f in list_flavors
348
                     if f['id'].lower() == flv_value.lower()]
349
            else:
350
                self.logger.error("Unrecognized flavor type %s" % flv_type)
351

    
352
            # Check if we found one
353
            if list_flvs:
354
                self.logger.debug("Will use \"%s\" with id \"%s\""
355
                                  % (list_flvs[0]['name'], list_flvs[0]['id']))
356
                return list_flvs[0]['id']
357

    
358
        self.logger.error("No matching flavor found.. aborting")
359
        sys.exit(1)
360

    
361
    def _find_image(self, image=None):
362
        """Find a suitable image to use
363

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

    
374
        list_images = self.image_client.list_public(detail=True)['images']
375
        for img in images:
376
            [img_type, img_value] = img.strip().split(':')
377
            if img_type == "name":
378
                # Filter images by name
379
                self.logger.debug(
380
                    "Trying to find an image with name \"%s\"" % img_value)
381
                list_imgs = \
382
                    [i for i in list_images
383
                     if i['user_id'] in DEFAULT_SYSTEM_IMAGES_UUID and
384
                        re.search(img_value, i['name'], flags=re.I) is not None]
385
            elif img_type == "id":
386
                # Filter images by id
387
                self.logger.debug(
388
                    "Trying to find an image with id \"%s\"" % img_value)
389
                list_imgs = \
390
                    [i for i in list_images
391
                     if i['id'].lower() == img_value.lower()]
392
            else:
393
                self.logger.error("Unrecognized image type %s" % img_type)
394
                sys.exit(1)
395

    
396
            # Check if we found one
397
            if list_imgs:
398
                self.logger.debug("Will use \"%s\" with id \"%s\""
399
                                  % (list_imgs[0]['name'], list_imgs[0]['id']))
400
                return list_imgs[0]['id']
401

    
402
        # We didn't found one
403
        self.logger.error("No matching image found.. aborting")
404
        sys.exit(1)
405

    
406
    def _get_server_ip_and_port(self, server):
407
        """Compute server's IPv4 and ssh port number"""
408
        self.logger.info("Get server connection details..")
409
        server_ip = server['attachments'][0]['ipv4']
410
        if ".okeanos.io" in self.cyclades_client.base_url:
411
            tmp1 = int(server_ip.split(".")[2])
412
            tmp2 = int(server_ip.split(".")[3])
413
            server_ip = "gate.okeanos.io"
414
            server_port = 10000 + tmp1 * 256 + tmp2
415
        else:
416
            server_port = 22
417
        self.write_temp_config('server_ip', server_ip)
418
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
419
        self.write_temp_config('server_port', server_port)
420
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
421
        self.logger.debug("Access server using \"ssh -X -p %s %s@%s\"" %
422
                          (server_port, server['metadata']['users'], server_ip))
423

    
424
    @_check_fabric
425
    def _copy_ssh_keys(self, ssh_keys):
426
        """Upload/Install ssh keys to server"""
427
        self.logger.debug("Check for authentication keys to use")
428
        if ssh_keys is None:
429
            ssh_keys = self.config.get("Deployment", "ssh_keys")
430

    
431
        if ssh_keys != "":
432
            ssh_keys = os.path.expanduser(ssh_keys)
433
            self.logger.debug("Will use %s authentication keys file" % ssh_keys)
434
            keyfile = '/tmp/%s.pub' % fabric.env.user
435
            _run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False)
436
            if ssh_keys.startswith("http://") or \
437
                    ssh_keys.startswith("https://") or \
438
                    ssh_keys.startswith("ftp://"):
439
                cmd = """
440
                apt-get update
441
                apt-get install wget --yes
442
                wget {0} -O {1} --no-check-certificate
443
                """.format(ssh_keys, keyfile)
444
                _run(cmd, False)
445
            elif os.path.exists(ssh_keys):
446
                _put(ssh_keys, keyfile)
447
            else:
448
                self.logger.debug("No ssh keys found")
449
                return
450
            _run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False)
451
            _run('rm %s' % keyfile, False)
452
            self.logger.debug("Uploaded ssh authorized keys")
453
        else:
454
            self.logger.debug("No ssh keys found")
455

    
456
    def write_temp_config(self, option, value):
457
        """Write changes back to config file"""
458
        # Acquire the lock to write to temp_config_file
459
        with filelocker.lock("%s.lock" % self.temp_config_file,
460
                             filelocker.LOCK_EX):
461

    
462
            # Read temp_config again to get any new entries
463
            self.temp_config.read(self.temp_config_file)
464

    
465
            # If build_id section doesn't exist create a new one
466
            try:
467
                self.temp_config.add_section(str(self.build_id))
468
                creation_time = \
469
                    time.strftime("%a, %d %b %Y %X", time.localtime())
470
                self.temp_config.set(str(self.build_id),
471
                                     "created", str(creation_time))
472
            except DuplicateSectionError:
473
                pass
474
            self.temp_config.set(str(self.build_id), option, str(value))
475
            curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
476
            self.temp_config.set(str(self.build_id), "modified", curr_time)
477
            with open(self.temp_config_file, 'wb') as tcf:
478
                self.temp_config.write(tcf)
479

    
480
    def read_temp_config(self, option):
481
        """Read from temporary_config file"""
482
        # If build_id is None use the latest one
483
        if self.build_id is None:
484
            ids = self.temp_config.sections()
485
            if ids:
486
                self.build_id = int(ids[-1])
487
            else:
488
                self.logger.error("No sections in temporary config file")
489
                sys.exit(1)
490
            self.logger.debug("Will use \"%s\" as build id"
491
                              % _green(self.build_id))
492
        # Read specified option
493
        return self.temp_config.get(str(self.build_id), option)
494

    
495
    def setup_fabric(self):
496
        """Setup fabric environment"""
497
        self.logger.info("Setup fabric parameters..")
498
        fabric.env.user = self.read_temp_config('server_user')
499
        fabric.env.host_string = self.read_temp_config('server_ip')
500
        fabric.env.port = int(self.read_temp_config('server_port'))
501
        fabric.env.password = self.read_temp_config('server_passwd')
502
        fabric.env.connection_attempts = 10
503
        fabric.env.shell = "/bin/bash -c"
504
        fabric.env.disable_known_hosts = True
505
        fabric.env.output_prefix = None
506

    
507
    def _check_hash_sum(self, localfile, remotefile):
508
        """Check hash sums of two files"""
509
        self.logger.debug("Check hash sum for local file %s" % localfile)
510
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
511
        self.logger.debug("Local file has sha256 hash %s" % hash1)
512
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
513
        hash2 = _run("sha256sum %s" % remotefile, False)
514
        hash2 = hash2.split(' ')[0]
515
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
516
        if hash1 != hash2:
517
            self.logger.error("Hashes differ.. aborting")
518
            sys.exit(-1)
519

    
520
    @_check_fabric
521
    def clone_repo(self, local_repo=False):
522
        """Clone Synnefo repo from slave server"""
523
        self.logger.info("Configure repositories on remote server..")
524
        self.logger.debug("Install/Setup git")
525
        cmd = """
526
        apt-get install git --yes
527
        git config --global user.name {0}
528
        git config --global user.email {1}
529
        """.format(self.config.get('Global', 'git_config_name'),
530
                   self.config.get('Global', 'git_config_mail'))
531
        _run(cmd, False)
532

    
533
        # Find synnefo_repo and synnefo_branch to use
534
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
535
        synnefo_branch = self.config.get("Global", "synnefo_branch")
536
        if synnefo_branch == "":
537
            synnefo_branch = \
538
                subprocess.Popen(
539
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
540
                    stdout=subprocess.PIPE).communicate()[0].strip()
541
            if synnefo_branch == "HEAD":
542
                synnefo_branch = \
543
                    subprocess.Popen(
544
                        ["git", "rev-parse", "--short", "HEAD"],
545
                        stdout=subprocess.PIPE).communicate()[0].strip()
546
        self.logger.info("Will use branch %s" % synnefo_branch)
547

    
548
        if local_repo or synnefo_branch == "":
549
            # Use local_repo
550
            self.logger.debug("Push local repo to server")
551
            # Firstly create the remote repo
552
            _run("git init synnefo", False)
553
            # Then push our local repo over ssh
554
            # We have to pass some arguments to ssh command
555
            # namely to disable host checking.
556
            (temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp()
557
            os.close(temp_ssh_file_handle)
558
            # XXX: git push doesn't read the password
559
            cmd = """
560
            echo 'exec ssh -o "StrictHostKeyChecking no" \
561
                           -o "UserKnownHostsFile /dev/null" \
562
                           -q "$@"' > {4}
563
            chmod u+x {4}
564
            export GIT_SSH="{4}"
565
            echo "{0}" | git push --mirror ssh://{1}@{2}:{3}/~/synnefo
566
            rm -f {4}
567
            """.format(fabric.env.password,
568
                       fabric.env.user,
569
                       fabric.env.host_string,
570
                       fabric.env.port,
571
                       temp_ssh_file)
572
            os.system(cmd)
573
        else:
574
            # Clone Synnefo from remote repo
575
            # Currently clonning synnefo can fail unexpectedly
576
            cloned = False
577
            for i in range(10):
578
                self.logger.debug("Clone synnefo from %s" % synnefo_repo)
579
                try:
580
                    _run("git clone %s synnefo" % synnefo_repo, False)
581
                    cloned = True
582
                    break
583
                except BaseException:
584
                    self.logger.warning(
585
                        "Clonning synnefo failed.. retrying %s" % i)
586
            if not cloned:
587
                self.logger.error("Can not clone Synnefo repo.")
588
                sys.exit(-1)
589

    
590
        # Checkout the desired synnefo_branch
591
        self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch)
592
        cmd = """
593
        cd synnefo
594
        for branch in `git branch -a | grep remotes | \
595
                       grep -v HEAD | grep -v master`; do
596
            git branch --track ${branch##*/} $branch
597
        done
598
        git checkout %s
599
        """ % (synnefo_branch)
600
        _run(cmd, False)
601

    
602
    @_check_fabric
603
    def build_synnefo(self):
604
        """Build Synnefo packages"""
605
        self.logger.info("Build Synnefo packages..")
606
        self.logger.debug("Install development packages")
607
        cmd = """
608
        apt-get update
609
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
610
                python-dev python-all python-pip --yes
611
        pip install devflow
612
        """
613
        _run(cmd, False)
614

    
615
        if self.config.get('Global', 'patch_pydist') == "True":
616
            self.logger.debug("Patch pydist.py module")
617
            cmd = r"""
618
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
619
                /usr/share/python/debpython/pydist.py
620
            """
621
            _run(cmd, False)
622

623
        # Build synnefo packages
624
        self.logger.debug("Build synnefo packages")
625
        cmd = """
626
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
627
        """
628
        with fabric.cd("synnefo"):
629
            _run(cmd, True)
630

631
        # Install snf-deploy package
632
        self.logger.debug("Install snf-deploy package")
633
        cmd = """
634
        dpkg -i snf-deploy*.deb
635
        apt-get -f install --yes
636
        """
637
        with fabric.cd("synnefo_build-area"):
638
            with fabric.settings(warn_only=True):
639
                _run(cmd, True)
640

641
        # Setup synnefo packages for snf-deploy
642
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
643
        cmd = """
644
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
645
        """
646
        _run(cmd, False)
647

648
    @_check_fabric
649
    def build_documentation(self):
650
        """Build Synnefo documentation"""
651
        self.logger.info("Build Synnefo documentation..")
652
        _run("pip install -U Sphinx", False)
653
        with fabric.cd("synnefo"):
654
            _run("devflow-update-version; "
655
                 "./ci/make_docs.sh synnefo_documentation", False)
656

657
    def fetch_documentation(self, dest=None):
658
        """Fetch Synnefo documentation"""
659
        self.logger.info("Fetch Synnefo documentation..")
660
        if dest is None:
661
            dest = "synnefo_documentation"
662
        dest = os.path.abspath(dest)
663
        if not os.path.exists(dest):
664
            os.makedirs(dest)
665
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
666
        self.logger.info("Downloaded documentation to %s" %
667
                         _green(dest))
668

669
    @_check_fabric
670
    def deploy_synnefo(self, schema=None):
671
        """Deploy Synnefo using snf-deploy"""
672
        self.logger.info("Deploy Synnefo..")
673
        if schema is None:
674
            schema = self.config.get('Global', 'schema')
675
        self.logger.debug("Will use \"%s\" schema" % schema)
676

677
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
678
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
679
            raise ValueError("Unknown schema: %s" % schema)
680

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

684
        self.logger.debug("Change password in nodes.conf file")
685
        cmd = """
686
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
687
        """.format(fabric.env.password)
688
        _run(cmd, False)
689

690
        self.logger.debug("Run snf-deploy")
691
        cmd = """
692
        snf-deploy --disable-colors --autoconf all
693
        """
694
        _run(cmd, True)
695

696
    @_check_fabric
697
    def unit_test(self):
698
        """Run Synnefo unit test suite"""
699
        self.logger.info("Run Synnefo unit test suite")
700
        component = self.config.get('Unit Tests', 'component')
701

702
        self.logger.debug("Install needed packages")
703
        cmd = """
704
        pip install mock
705
        pip install factory_boy
706
        """
707
        _run(cmd, False)
708

709
        self.logger.debug("Upload tests.sh file")
710
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
711
        _put(unit_tests_file, ".")
712

713
        self.logger.debug("Run unit tests")
714
        cmd = """
715
        bash tests.sh {0}
716
        """.format(component)
717
        _run(cmd, True)
718

719
    @_check_fabric
720
    def run_burnin(self):
721
        """Run burnin functional test suite"""
722
        self.logger.info("Run Burnin functional test suite")
723
        cmd = """
724
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
725
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
726
        images_user=$(kamaki image list -l | grep owner | \
727
                      cut -d':' -f2 | tr -d ' ')
728
        snf-burnin --auth-url=$auth_url --token=$token \
729
            --force-flavor=2 --image-id=all \
730
            --system-images-user=$images_user \
731
            {0}
732
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
733
        for i in $(ls $log_folder/*/details*); do
734
            echo -e "\\n\\n"
735
            echo -e "***** $i\\n"
736
            cat $i
737
        done
738
        """.format(self.config.get('Burnin', 'cmd_options'))
739
        _run(cmd, True)
740

741
    @_check_fabric
742
    def fetch_compressed(self, src, dest=None):
743
        """Create a tarball and fetch it locally"""
744
        self.logger.debug("Creating tarball of %s" % src)
745
        basename = os.path.basename(src)
746
        tar_file = basename + ".tgz"
747
        cmd = "tar czf %s %s" % (tar_file, src)
748
        _run(cmd, False)
749
        if not os.path.exists(dest):
750
            os.makedirs(dest)
751

752
        tmp_dir = tempfile.mkdtemp()
753
        fabric.get(tar_file, tmp_dir)
754

755
        dest_file = os.path.join(tmp_dir, tar_file)
756
        self._check_hash_sum(dest_file, tar_file)
757
        self.logger.debug("Untar packages file %s" % dest_file)
758
        cmd = """
759
        cd %s
760
        tar xzf %s
761
        cp -r %s/* %s
762
        rm -r %s
763
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
764
        os.system(cmd)
765
        self.logger.info("Downloaded %s to %s" %
766
                         (src, _green(dest)))
767

768
    @_check_fabric
769
    def fetch_packages(self, dest=None):
770
        """Fetch Synnefo packages"""
771
        if dest is None:
772
            dest = self.config.get('Global', 'pkgs_dir')
773
        dest = os.path.abspath(os.path.expanduser(dest))
774
        if not os.path.exists(dest):
775
            os.makedirs(dest)
776
        self.fetch_compressed("synnefo_build-area", dest)
777
        self.logger.info("Downloaded debian packages to %s" %
778
                         _green(dest))
779