Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 33ad9a0d

History | View | Annotate | Download (21.3 kB)

1
#!/usr/bin/env python
2

    
3
"""
4
Synnefo ci utils module
5
"""
6

    
7
import os
8
import sys
9
import time
10
import logging
11
import fabric.api as fabric
12
import subprocess
13
import tempfile
14
from ConfigParser import ConfigParser, DuplicateSectionError
15

    
16
from kamaki.cli import config as kamaki_config
17
from kamaki.clients.astakos import AstakosClient
18
from kamaki.clients.cyclades import CycladesClient
19
from kamaki.clients.image import ImageClient
20

    
21
DEFAULT_CONFIG_FILE = "new_config"
22

    
23

    
24
def _run(cmd, verbose):
25
    """Run fabric with verbose level"""
26
    if verbose:
27
        args = ('running',)
28
    else:
29
        args = ('running', 'stdout',)
30
    with fabric.hide(*args):  # Used * or ** magic. pylint: disable-msg=W0142
31
        return fabric.run(cmd)
32

    
33

    
34
def _red(msg):
35
    """Red color"""
36
    #return "\x1b[31m" + str(msg) + "\x1b[0m"
37
    return str(msg)
38

    
39

    
40
def _yellow(msg):
41
    """Yellow color"""
42
    #return "\x1b[33m" + str(msg) + "\x1b[0m"
43
    return str(msg)
44

    
45

    
46
def _green(msg):
47
    """Green color"""
48
    #return "\x1b[32m" + str(msg) + "\x1b[0m"
49
    return str(msg)
50

    
51

    
52
def _check_fabric(fun):
53
    """Check if fabric env has been set"""
54
    def wrapper(self, *args, **kwargs):
55
        """wrapper function"""
56
        if not self.fabric_installed:
57
            self.setup_fabric()
58
        return fun(self, *args, **kwargs)
59
    return wrapper
60

    
61

    
62
def _check_kamaki(fun):
63
    """Check if kamaki has been initialized"""
64
    def wrapper(self, *args, **kwargs):
65
        """wrapper function"""
66
        if not self.kamaki_installed:
67
            self.setup_kamaki()
68
        return fun(self, *args, **kwargs)
69
    return wrapper
70

    
71

    
72
class _MyFormatter(logging.Formatter):
73
    """Logging Formatter"""
74
    def format(self, record):
75
        format_orig = self._fmt
76
        if record.levelno == logging.DEBUG:
77
            self._fmt = "  %(msg)s"
78
        elif record.levelno == logging.INFO:
79
            self._fmt = "%(msg)s"
80
        elif record.levelno == logging.WARNING:
81
            self._fmt = _yellow("[W] %(msg)s")
82
        elif record.levelno == logging.ERROR:
83
            self._fmt = _red("[E] %(msg)s")
84
        result = logging.Formatter.format(self, record)
85
        self._fmt = format_orig
86
        return result
87

    
88

    
89
class SynnefoCI(object):
90
    """SynnefoCI python class"""
91

    
92
    def __init__(self, config_file=None, cleanup_config=False, cloud=None):
93
        """ Initialize SynnefoCI python class
94

95
        Setup logger, local_dir, config and kamaki
96
        """
97
        # Setup logger
98
        self.logger = logging.getLogger('synnefo-ci')
99
        self.logger.setLevel(logging.DEBUG)
100
        handler = logging.StreamHandler()
101
        handler.setFormatter(_MyFormatter())
102
        self.logger.addHandler(handler)
103

    
104
        # Get our local dir
105
        self.ci_dir = os.path.dirname(os.path.abspath(__file__))
106
        self.repo_dir = os.path.dirname(self.ci_dir)
107

    
108
        # Read config file
109
        if config_file is None:
110
            config_file = DEFAULT_CONFIG_FILE
111
        if not os.path.isabs(config_file):
112
            config_file = os.path.join(self.ci_dir, config_file)
113

    
114
        self.config = ConfigParser()
115
        self.config.optionxform = str
116
        self.config.read(config_file)
117
        temp_config = self.config.get('Global', 'temporary_config')
118
        if cleanup_config:
119
            try:
120
                os.remove(temp_config)
121
            except OSError:
122
                pass
123
        else:
124
            self.config.read(self.config.get('Global', 'temporary_config'))
125

    
126
        # Set kamaki cloud
127
        if cloud is not None:
128
            self.kamaki_cloud = cloud
129
        elif self.config.has_option("Deployment", "kamaki_cloud"):
130
            kamaki_cloud = self.config.get("Deployment", "kamaki_cloud")
131
            if kamaki_cloud == "":
132
                self.kamaki_cloud = None
133
        else:
134
            self.kamaki_cloud = None
135

    
136
        # Initialize variables
137
        self.fabric_installed = False
138
        self.kamaki_installed = False
139
        self.cyclades_client = None
140
        self.image_client = None
141

    
142
    def setup_kamaki(self):
143
        """Initialize kamaki
144

145
        Setup cyclades_client and image_client
146
        """
147

    
148
        config = kamaki_config.Config()
149
        if self.kamaki_cloud is None:
150
            self.kamaki_cloud = config.get_global("default_cloud")
151

    
152
        self.logger.info("Setup kamaki client, using cloud '%s'.." %
153
                         self.kamaki_cloud)
154
        auth_url = config.get_cloud(self.kamaki_cloud, "url")
155
        self.logger.debug("Authentication URL is %s" % _green(auth_url))
156
        token = config.get_cloud(self.kamaki_cloud, "token")
157
        #self.logger.debug("Token is %s" % _green(token))
158

    
159
        astakos_client = AstakosClient(auth_url, token)
160

    
161
        cyclades_url = \
162
            astakos_client.get_service_endpoints('compute')['publicURL']
163
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
164
        self.cyclades_client = CycladesClient(cyclades_url, token)
165
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
166

    
167
        image_url = \
168
            astakos_client.get_service_endpoints('image')['publicURL']
169
        self.logger.debug("Images API url is %s" % _green(image_url))
170
        self.image_client = ImageClient(cyclades_url, token)
171
        self.image_client.CONNECTION_RETRY_LIMIT = 2
172

    
173
    def _wait_transition(self, server_id, current_status, new_status):
174
        """Wait for server to go from current_status to new_status"""
175
        self.logger.debug("Waiting for server to become %s" % new_status)
176
        timeout = self.config.getint('Global', 'build_timeout')
177
        sleep_time = 5
178
        while True:
179
            server = self.cyclades_client.get_server_details(server_id)
180
            if server['status'] == new_status:
181
                return server
182
            elif timeout < 0:
183
                self.logger.error(
184
                    "Waiting for server to become %s timed out" % new_status)
185
                self.destroy_server(False)
186
                sys.exit(-1)
187
            elif server['status'] == current_status:
188
                # Sleep for #n secs and continue
189
                timeout = timeout - sleep_time
190
                time.sleep(sleep_time)
191
            else:
192
                self.logger.error(
193
                    "Server failed with status %s" % server['status'])
194
                self.destroy_server(False)
195
                sys.exit(-1)
196

    
197
    @_check_kamaki
198
    def destroy_server(self, wait=True):
199
        """Destroy slave server"""
200
        server_id = self.config.getint('Temporary Options', 'server_id')
201
        self.logger.info("Destoying server with id %s " % server_id)
202
        self.cyclades_client.delete_server(server_id)
203
        if wait:
204
            self._wait_transition(server_id, "ACTIVE", "DELETED")
205

    
206
    @_check_kamaki
207
    def create_server(self, image_id=None, flavor_id=None):
208
        """Create slave server"""
209
        self.logger.info("Create a new server..")
210
        if image_id is None:
211
            image = self._find_image()
212
            self.logger.debug("Will use image \"%s\"" % _green(image['name']))
213
            image_id = image["id"]
214
        self.logger.debug("Image has id %s" % _green(image_id))
215
        if flavor_id is None:
216
            flavor_id = self.config.getint("Deployment", "flavor_id")
217
        server = self.cyclades_client.create_server(
218
            self.config.get('Deployment', 'server_name'),
219
            flavor_id,
220
            image_id)
221
        server_id = server['id']
222
        self.write_config('server_id', server_id)
223
        self.logger.debug("Server got id %s" % _green(server_id))
224
        server_user = server['metadata']['users']
225
        self.write_config('server_user', server_user)
226
        self.logger.debug("Server's admin user is %s" % _green(server_user))
227
        server_passwd = server['adminPass']
228
        self.write_config('server_passwd', server_passwd)
229

    
230
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
231
        self._get_server_ip_and_port(server)
232
        self._copy_ssh_keys()
233

    
234
        self.setup_fabric()
235
        self.logger.info("Setup firewall")
236
        accept_ssh_from = self.config.get('Global', 'filter_access_network')
237
        if accept_ssh_from != "":
238
            self.logger.debug("Block ssh except from %s" % accept_ssh_from)
239
            cmd = """
240
            local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
241
                cut -d':' -f2 | cut -d' ' -f1)
242
            iptables -A INPUT -s localhost -j ACCEPT
243
            iptables -A INPUT -s $local_ip -j ACCEPT
244
            iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
245
            iptables -A INPUT -p tcp --dport 22 -j DROP
246
            """.format(accept_ssh_from)
247
            _run(cmd, False)
248

    
249
    def _find_image(self):
250
        """Find a suitable image to use
251

252
        It has to belong to the `system_uuid' user and
253
        contain the word `image_name'
254
        """
255
        system_uuid = self.config.get('Deployment', 'system_uuid')
256
        image_name = self.config.get('Deployment', 'image_name').lower()
257
        images = self.image_client.list_public(detail=True)['images']
258
        # Select images by `system_uuid' user
259
        images = [x for x in images if x['user_id'] == system_uuid]
260
        # Select images with `image_name' in their names
261
        images = \
262
            [x for x in images if x['name'].lower().find(image_name) != -1]
263
        # Let's select the first one
264
        return images[0]
265

    
266
    def _get_server_ip_and_port(self, server):
267
        """Compute server's IPv4 and ssh port number"""
268
        self.logger.info("Get server connection details..")
269
        server_ip = server['attachments'][0]['ipv4']
270
        if ".okeanos.io" in self.cyclades_client.base_url:
271
            tmp1 = int(server_ip.split(".")[2])
272
            tmp2 = int(server_ip.split(".")[3])
273
            server_ip = "gate.okeanos.io"
274
            server_port = 10000 + tmp1 * 256 + tmp2
275
        else:
276
            server_port = 22
277
        self.write_config('server_ip', server_ip)
278
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
279
        self.write_config('server_port', server_port)
280
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
281

    
282
    @_check_fabric
283
    def _copy_ssh_keys(self):
284
        """Upload/Install ssh keys to server"""
285
        if not self.config.has_option("Deployment", "ssh_keys"):
286
            return
287
        authorized_keys = self.config.get("Deployment",
288
                                          "ssh_keys")
289
        if authorized_keys != "" and os.path.exists(authorized_keys):
290
            keyfile = '/tmp/%s.pub' % fabric.env.user
291
            _run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False)
292
            fabric.put(authorized_keys, keyfile)
293
            _run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False)
294
            _run('rm %s' % keyfile, False)
295
            self.logger.debug("Uploaded ssh authorized keys")
296
        else:
297
            self.logger.debug("No ssh keys found")
298

    
299
    def write_config(self, option, value, section="Temporary Options"):
300
        """Write changes back to config file"""
301
        try:
302
            self.config.add_section(section)
303
        except DuplicateSectionError:
304
            pass
305
        self.config.set(section, option, str(value))
306
        temp_conf_file = self.config.get('Global', 'temporary_config')
307
        with open(temp_conf_file, 'wb') as tcf:
308
            self.config.write(tcf)
309

    
310
    def setup_fabric(self):
311
        """Setup fabric environment"""
312
        self.logger.info("Setup fabric parameters..")
313
        fabric.env.user = self.config.get('Temporary Options', 'server_user')
314
        fabric.env.host_string = \
315
            self.config.get('Temporary Options', 'server_ip')
316
        fabric.env.port = self.config.getint('Temporary Options',
317
                                             'server_port')
318
        fabric.env.password = self.config.get('Temporary Options',
319
                                              'server_passwd')
320
        fabric.env.connection_attempts = 10
321
        fabric.env.shell = "/bin/bash -c"
322
        fabric.env.disable_known_hosts = True
323
        fabric.env.output_prefix = None
324

    
325
    def _check_hash_sum(self, localfile, remotefile):
326
        """Check hash sums of two files"""
327
        self.logger.debug("Check hash sum for local file %s" % localfile)
328
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
329
        self.logger.debug("Local file has sha256 hash %s" % hash1)
330
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
331
        hash2 = _run("sha256sum %s" % remotefile, False)
332
        hash2 = hash2.split(' ')[0]
333
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
334
        if hash1 != hash2:
335
            self.logger.error("Hashes differ.. aborting")
336
            sys.exit(-1)
337

    
338
    @_check_fabric
339
    def clone_repo(self):
340
        """Clone Synnefo repo from slave server"""
341
        self.logger.info("Configure repositories on remote server..")
342
        self.logger.debug("Setup apt, install curl and git")
343
        cmd = """
344
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
345
        apt-get update
346
        apt-get install curl git --yes
347
        echo -e "\n\ndeb {0}" >> /etc/apt/sources.list
348
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
349
        apt-get update
350
        git config --global user.name {1}
351
        git config --global user.email {2}
352
        """.format(self.config.get('Global', 'apt_repo'),
353
                   self.config.get('Global', 'git_config_name'),
354
                   self.config.get('Global', 'git_config_mail'))
355
        _run(cmd, False)
356

    
357
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
358
        synnefo_branch = self.config.get("Global", "synnefo_branch")
359
        if synnefo_branch == "":
360
            synnefo_branch = \
361
                subprocess.Popen(
362
                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
363
                    stdout=subprocess.PIPE).communicate()[0].strip()
364
            if synnefo_branch == "HEAD":
365
                synnefo_branch = \
366
                    subprocess.Popen(
367
                        ["git", "rev-parse", "--short", "HEAD"],
368
                        stdout=subprocess.PIPE).communicate()[0].strip()
369
        self.logger.info("Will use branch %s" % synnefo_branch)
370
        # Currently clonning synnefo can fail unexpectedly
371
        cloned = False
372
        for i in range(10):
373
            self.logger.debug("Clone synnefo from %s" % synnefo_repo)
374
            try:
375
                _run("git clone %s synnefo" % synnefo_repo, False)
376
                cloned = True
377
                break
378
            except BaseException:
379
                self.logger.warning("Clonning synnefo failed.. retrying %s"
380
                                    % i)
381
        cmd = """
382
        cd synnefo
383
        for branch in `git branch -a | grep remotes | \
384
                       grep -v HEAD | grep -v master`; do
385
            git branch --track ${branch##*/} $branch
386
        done
387
        git checkout %s
388
        """ % (synnefo_branch)
389
        _run(cmd, False)
390

    
391
        if not cloned:
392
            self.logger.error("Can not clone Synnefo repo.")
393
            sys.exit(-1)
394

    
395
        deploy_repo = self.config.get('Global', 'deploy_repo')
396
        self.logger.debug("Clone snf-deploy from %s" % deploy_repo)
397
        _run("git clone --depth 1 %s" % deploy_repo, False)
398

    
399
    @_check_fabric
400
    def build_synnefo(self):
401
        """Build Synnefo packages"""
402
        self.logger.info("Build Synnefo packages..")
403
        self.logger.debug("Install development packages")
404
        cmd = """
405
        apt-get update
406
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
407
                python-dev python-all python-pip --yes
408
        pip install devflow
409
        """
410
        _run(cmd, False)
411

    
412
        if self.config.get('Global', 'patch_pydist') == "True":
413
            self.logger.debug("Patch pydist.py module")
414
            cmd = r"""
415
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
416
                /usr/share/python/debpython/pydist.py
417
            """
418
            _run(cmd, False)
419

420
        self.logger.debug("Build snf-deploy package")
421
        cmd = """
422
        git checkout -t origin/debian
423
        git-buildpackage --git-upstream-branch=master \
424
                --git-debian-branch=debian \
425
                --git-export-dir=../snf-deploy_build-area \
426
                -uc -us
427
        """
428
        with fabric.cd("snf-deploy"):
429
            _run(cmd, True)
430

431
        self.logger.debug("Install snf-deploy package")
432
        cmd = """
433
        dpkg -i snf-deploy*.deb
434
        apt-get -f install --yes
435
        """
436
        with fabric.cd("snf-deploy_build-area"):
437
            with fabric.settings(warn_only=True):
438
                _run(cmd, True)
439

440
        self.logger.debug("Build synnefo packages")
441
        cmd = """
442
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
443
        """
444
        with fabric.cd("synnefo"):
445
            _run(cmd, True)
446

447
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
448
        cmd = """
449
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
450
        """
451
        _run(cmd, False)
452

453
    @_check_fabric
454
    def build_documentation(self):
455
        """Build Synnefo documentation"""
456
        self.logger.info("Build Synnefo documentation..")
457
        _run("pip install -U Sphinx", False)
458
        with fabric.cd("synnefo"):
459
            _run("devflow-update-version; "
460
                 "./ci/make_docs.sh synnefo_documentation", False)
461

462
    def fetch_documentation(self, dest=None):
463
        """Fetch Synnefo documentation"""
464
        self.logger.info("Fetch Synnefo documentation..")
465
        if dest is None:
466
            dest = "synnefo_documentation"
467
        dest = os.path.abspath(dest)
468
        if not os.path.exists(dest):
469
            os.makedirs(dest)
470
        self.fetch_compressed("synnefo/synnefo_documentation", dest)
471
        self.logger.info("Downloaded documentation to %s" %
472
                         _green(dest))
473

474
    @_check_fabric
475
    def deploy_synnefo(self, schema=None):
476
        """Deploy Synnefo using snf-deploy"""
477
        self.logger.info("Deploy Synnefo..")
478
        if schema is None:
479
            schema = self.config.get('Global', 'schema')
480
        self.logger.debug("Will use %s schema" % schema)
481

482
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
483
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
484
            raise ValueError("Unknown schema: %s" % schema)
485

486
        self.logger.debug("Upload schema files to server")
487
        with fabric.quiet():
488
            fabric.put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/")
489

490
        self.logger.debug("Change password in nodes.conf file")
491
        cmd = """
492
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
493
        """.format(fabric.env.password)
494
        _run(cmd, False)
495

496
        self.logger.debug("Run snf-deploy")
497
        cmd = """
498
        snf-deploy all --autoconf
499
        """
500
        _run(cmd, True)
501

502
    @_check_fabric
503
    def unit_test(self):
504
        """Run Synnefo unit test suite"""
505
        self.logger.info("Run Synnefo unit test suite")
506
        component = self.config.get('Unit Tests', 'component')
507

508
        self.logger.debug("Install needed packages")
509
        cmd = """
510
        pip install mock
511
        pip install factory_boy
512
        """
513
        _run(cmd, False)
514

515
        self.logger.debug("Upload tests.sh file")
516
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
517
        with fabric.quiet():
518
            fabric.put(unit_tests_file, ".")
519

520
        self.logger.debug("Run unit tests")
521
        cmd = """
522
        bash tests.sh {0}
523
        """.format(component)
524
        _run(cmd, True)
525

526
    @_check_fabric
527
    def run_burnin(self):
528
        """Run burnin functional test suite"""
529
        self.logger.info("Run Burnin functional test suite")
530
        cmd = """
531
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
532
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
533
        images_user=$(kamaki image list -l | grep owner | \
534
                      cut -d':' -f2 | tr -d ' ')
535
        snf-burnin --auth-url=$auth_url --token=$token \
536
            --force-flavor=2 --image-id=all \
537
            --system-images-user=$images_user \
538
            {0}
539
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
540
        for i in $(ls $log_folder/*/details*); do
541
            echo -e "\\n\\n"
542
            echo -e "***** $i\\n"
543
            cat $i
544
        done
545
        """.format(self.config.get('Burnin', 'cmd_options'))
546
        _run(cmd, True)
547

548
    @_check_fabric
549
    def fetch_compressed(self, src, dest=None):
550
        """Create a tarball and fetch it locally"""
551
        self.logger.debug("Creating tarball of %s" % src)
552
        basename = os.path.basename(src)
553
        tar_file = basename + ".tgz"
554
        cmd = "tar czf %s %s" % (tar_file, src)
555
        _run(cmd, False)
556
        if not os.path.exists(dest):
557
            os.makedirs(dest)
558

559
        tmp_dir = tempfile.mkdtemp()
560
        fabric.get(tar_file, tmp_dir)
561

562
        dest_file = os.path.join(tmp_dir, tar_file)
563
        self._check_hash_sum(dest_file, tar_file)
564
        self.logger.debug("Untar packages file %s" % dest_file)
565
        cmd = """
566
        cd %s
567
        tar xzf %s
568
        cp -r %s/* %s
569
        rm -r %s
570
        """ % (tmp_dir, tar_file, src, dest, tmp_dir)
571
        os.system(cmd)
572
        self.logger.info("Downloaded %s to %s" %
573
                         (src, _green(dest)))
574

575
    @_check_fabric
576
    def fetch_packages(self, dest=None):
577
        """Fetch Synnefo packages"""
578
        if dest is None:
579
            dest = self.config.get('Global', 'pkgs_dir')
580
        dest = os.path.abspath(dest)
581
        if not os.path.exists(dest):
582
            os.makedirs(dest)
583
        self.fetch_compressed("synnefo_build-area", dest)
584
        self.logger.info("Downloaded debian packages to %s" %
585
                         _green(dest))
586