Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ b69b55ca

History | View | Annotate | Download (19.9 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
from ConfigParser import ConfigParser, DuplicateSectionError
14

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

    
20
DEFAULT_CONFIG_FILE = "new_config"
21

    
22

    
23
def _run(cmd, verbose):
24
    """Run fabric with verbose level"""
25
    if verbose:
26
        args = ('running',)
27
    else:
28
        args = ('running', 'stdout',)
29
    with fabric.hide(*args):
30
        return fabric.run(cmd)
31

    
32

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

    
38

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

    
44

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

    
50

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

    
60

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

    
70

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

    
87

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

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

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

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

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

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

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

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

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

144
        Setup cyclades_client and image_client
145
        """
146

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

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

    
158
        astakos_client = AstakosClient(auth_url, token)
159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
386
        if not cloned:
387
            self.logger.error("Can not clone Synnefo repo.")
388
            sys.exit(-1)
389

    
390
        deploy_repo = self.config.get('Global', 'deploy_repo')
391
        self.logger.debug("Clone snf-deploy from %s" % deploy_repo)
392
        _run("git clone --depth 1 %s" % deploy_repo, False)
393

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

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

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

426
        self.logger.debug("Install snf-deploy package")
427
        cmd = """
428
        dpkg -i snf-deploy*.deb
429
        apt-get -f install --yes
430
        """
431
        with fabric.cd("snf-deploy_build-area"):
432
            with fabric.settings(warn_only=True):
433
                _run(cmd, True)
434

435
        self.logger.debug("Build synnefo packages")
436
        cmd = """
437
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
438
        """
439
        with fabric.cd("synnefo"):
440
            _run(cmd, True)
441

442
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
443
        cmd = """
444
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
445
        """
446
        _run(cmd, False)
447

448
    @_check_fabric
449
    def deploy_synnefo(self, schema=None):
450
        """Deploy Synnefo using snf-deploy"""
451
        self.logger.info("Deploy Synnefo..")
452
        if schema is None:
453
            schema = self.config.get('Global', 'schema')
454
        self.logger.debug("Will use %s schema" % schema)
455

456
        schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
457
        if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
458
            raise ValueError("Unknown schema: %s" % schema)
459

460
        self.logger.debug("Upload schema files to server")
461
        with fabric.quiet():
462
            fabric.put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/")
463

464
        self.logger.debug("Change password in nodes.conf file")
465
        cmd = """
466
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
467
        """.format(fabric.env.password)
468
        _run(cmd, False)
469

470
        self.logger.debug("Run snf-deploy")
471
        cmd = """
472
        snf-deploy all --autoconf
473
        """
474
        _run(cmd, True)
475

476
    @_check_fabric
477
    def unit_test(self):
478
        """Run Synnefo unit test suite"""
479
        self.logger.info("Run Synnefo unit test suite")
480
        component = self.config.get('Unit Tests', 'component')
481

482
        self.logger.debug("Install needed packages")
483
        cmd = """
484
        pip install mock
485
        pip install factory_boy
486
        """
487
        _run(cmd, False)
488

489
        self.logger.debug("Upload tests.sh file")
490
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
491
        with fabric.quiet():
492
            fabric.put(unit_tests_file, ".")
493

494
        self.logger.debug("Run unit tests")
495
        cmd = """
496
        bash tests.sh {0}
497
        """.format(component)
498
        _run(cmd, True)
499

500
    @_check_fabric
501
    def run_burnin(self):
502
        """Run burnin functional test suite"""
503
        self.logger.info("Run Burnin functional test suite")
504
        cmd = """
505
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
506
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
507
        images_user=$(kamaki image list -l | grep owner | \
508
                      cut -d':' -f2 | tr -d ' ')
509
        snf-burnin --auth-url=$auth_url --token=$token \
510
            --force-flavor=2 --image-id=all \
511
            --system-images-user=$images_user \
512
            {0}
513
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
514
        for i in $(ls $log_folder/*/details*); do
515
            echo -e "\\n\\n"
516
            echo -e "***** $i\\n"
517
            cat $i
518
        done
519
        """.format(self.config.get('Burnin', 'cmd_options'))
520
        _run(cmd, True)
521

522
    @_check_fabric
523
    def fetch_packages(self):
524
        """Download Synnefo packages"""
525
        self.logger.info("Download Synnefo packages")
526
        self.logger.debug("Create tarball with packages")
527
        cmd = """
528
        tar czf synnefo_build-area.tgz synnefo_build-area
529
        """
530
        _run(cmd, False)
531

532
        pkgs_dir = self.config.get('Global', 'pkgs_dir')
533
        self.logger.debug("Fetch packages to local dir %s" % pkgs_dir)
534
        os.makedirs(pkgs_dir)
535
        with fabric.quiet():
536
            fabric.get("synnefo_build-area.tgz", pkgs_dir)
537

538
        pkgs_file = os.path.join(pkgs_dir, "synnefo_build-area.tgz")
539
        self._check_hash_sum(pkgs_file, "synnefo_build-area.tgz")
540

541
        self.logger.debug("Untar packages file %s" % pkgs_file)
542
        os.system("cd %s; tar xzf synnefo_build-area.tgz" % pkgs_dir)
543
        self.logger.info("Downloaded debian packages to %s" %
544
                         _green(pkgs_dir))
545