Statistics
| Branch: | Tag: | Revision:

root / ci / utils.py @ 133e5a5b

History | View | Annotate | Download (17.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
from ConfigParser import ConfigParser, DuplicateSectionError
13

    
14
from kamaki.clients.astakos import AstakosClient
15
from kamaki.clients.cyclades import CycladesClient
16
from kamaki.clients.image import ImageClient
17

    
18

    
19
def _run(cmd, verbose):
20
    """Run fabric with verbose level"""
21
    if verbose:
22
        args = ('running',)
23
    else:
24
        args = ('running', 'stdout',)
25
    with fabric.hide(*args):
26
        return fabric.run(cmd)
27

    
28

    
29
def _red(msg):
30
    """Red color"""
31
    #return "\x1b[31m" + str(msg) + "\x1b[0m"
32
    return str(msg)
33

    
34

    
35
def _yellow(msg):
36
    """Yellow color"""
37
    #return "\x1b[33m" + str(msg) + "\x1b[0m"
38
    return str(msg)
39

    
40

    
41
def _green(msg):
42
    """Green color"""
43
    #return "\x1b[32m" + str(msg) + "\x1b[0m"
44
    return str(msg)
45

    
46

    
47
def _check_fabric(fun):
48
    """Check if fabric env has been set"""
49
    def wrapper(self, *args):
50
        """wrapper function"""
51
        if not self.fabric_installed:
52
            self.setup_fabric()
53
        return fun(self, *args)
54
    return wrapper
55

    
56

    
57
def _check_kamaki(fun):
58
    """Check if kamaki has been initialized"""
59
    def wrapper(self, *args):
60
        """wrapper function"""
61
        if not self.kamaki_installed:
62
            self.setup_kamaki()
63
        return fun(self, *args)
64
    return wrapper
65

    
66

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

    
83

    
84
class SynnefoCI(object):
85
    """SynnefoCI python class"""
86

    
87
    def __init__(self, cleanup_config=False):
88
        """ Initialize SynnefoCI python class
89

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

    
99
        # Get our local dir
100
        self.ci_dir = os.path.dirname(os.path.abspath(__file__))
101
        self.repo_dir = os.path.dirname(self.ci_dir)
102

    
103
        # Read config file
104
        default_conffile = os.path.join(self.ci_dir, "new_config")
105
        self.conffile = os.environ.get("SYNNEFO_CI_CONFIG_FILE",
106
                                       default_conffile)
107

    
108
        self.config = ConfigParser()
109
        self.config.optionxform = str
110
        self.config.read(self.conffile)
111
        temp_config = self.config.get('Global', 'temporary_config')
112
        if cleanup_config:
113
            try:
114
                os.remove(temp_config)
115
            except:
116
                pass
117
        else:
118
            self.config.read(self.config.get('Global', 'temporary_config'))
119

    
120
        # Initialize variables
121
        self.fabric_installed = False
122
        self.kamaki_installed = False
123
        self.cyclades_client = None
124
        self.image_client = None
125

    
126
    def setup_kamaki(self):
127
        """Initialize kamaki
128

129
        Setup cyclades_client and image_client
130
        """
131
        self.logger.info("Setup kamaki client..")
132
        auth_url = self.config.get('Deployment', 'auth_url')
133
        self.logger.debug("Authentication URL is %s" % _green(auth_url))
134
        token = self.config.get('Deployment', 'token')
135
        #self.logger.debug("Token is %s" % _green(token))
136

    
137
        astakos_client = AstakosClient(auth_url, token)
138

    
139
        cyclades_url = \
140
            astakos_client.get_service_endpoints('compute')['publicURL']
141
        self.logger.debug("Cyclades API url is %s" % _green(cyclades_url))
142
        self.cyclades_client = CycladesClient(cyclades_url, token)
143
        self.cyclades_client.CONNECTION_RETRY_LIMIT = 2
144

    
145
        image_url = \
146
            astakos_client.get_service_endpoints('image')['publicURL']
147
        self.logger.debug("Images API url is %s" % _green(image_url))
148
        self.image_client = ImageClient(cyclades_url, token)
149
        self.image_client.CONNECTION_RETRY_LIMIT = 2
150

    
151
    def _wait_transition(self, server_id, current_status, new_status):
152
        """Wait for server to go from current_status to new_status"""
153
        self.logger.debug("Waiting for server to become %s" % new_status)
154
        timeout = self.config.getint('Global', 'build_timeout')
155
        sleep_time = 5
156
        while True:
157
            server = self.cyclades_client.get_server_details(server_id)
158
            if server['status'] == new_status:
159
                return server
160
            elif timeout < 0:
161
                self.logger.error(
162
                    "Waiting for server to become %s timed out" % new_status)
163
                self.destroy_server(False)
164
                sys.exit(-1)
165
            elif server['status'] == current_status:
166
                # Sleep for #n secs and continue
167
                timeout = timeout - sleep_time
168
                time.sleep(sleep_time)
169
            else:
170
                self.logger.error(
171
                    "Server failed with status %s" % server['status'])
172
                self.destroy_server(False)
173
                sys.exit(-1)
174

    
175
    @_check_kamaki
176
    def destroy_server(self, wait=True):
177
        """Destroy slave server"""
178
        server_id = self.config.getint('Temporary Options', 'server_id')
179
        self.logger.info("Destoying server with id %s " % server_id)
180
        self.cyclades_client.delete_server(server_id)
181
        if wait:
182
            self._wait_transition(server_id, "ACTIVE", "DELETED")
183

    
184
    @_check_kamaki
185
    def create_server(self):
186
        """Create slave server"""
187
        self.logger.info("Create a new server..")
188
        image = self._find_image()
189
        self.logger.debug("Will use image \"%s\"" % _green(image['name']))
190
        self.logger.debug("Image has id %s" % _green(image['id']))
191
        server = self.cyclades_client.create_server(
192
            self.config.get('Deployment', 'server_name'),
193
            self.config.getint('Deployment', 'flavor_id'),
194
            image['id'])
195
        server_id = server['id']
196
        self.write_config('server_id', server_id)
197
        self.logger.debug("Server got id %s" % _green(server_id))
198
        server_user = server['metadata']['users']
199
        self.write_config('server_user', server_user)
200
        self.logger.debug("Server's admin user is %s" % _green(server_user))
201
        server_passwd = server['adminPass']
202
        self.write_config('server_passwd', server_passwd)
203

    
204
        server = self._wait_transition(server_id, "BUILD", "ACTIVE")
205
        self._get_server_ip_and_port(server)
206

    
207
        self.setup_fabric()
208
        self.logger.info("Setup firewall")
209
        accept_ssh_from = self.config.get('Global', 'filter_access_network')
210
        self.logger.debug("Block ssh except from %s" % accept_ssh_from)
211
        cmd = """
212
        local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
213
            cut -d':' -f2 | cut -d' ' -f1)
214
        iptables -A INPUT -s localhost -j ACCEPT
215
        iptables -A INPUT -s $local_ip -j ACCEPT
216
        iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
217
        iptables -A INPUT -p tcp --dport 22 -j DROP
218
        """.format(accept_ssh_from)
219
        _run(cmd, False)
220

    
221
    def _find_image(self):
222
        """Find a suitable image to use
223

224
        It has to belong to the `system_uuid' user and
225
        contain the word `image_name'
226
        """
227
        system_uuid = self.config.get('Deployment', 'system_uuid')
228
        image_name = self.config.get('Deployment', 'image_name').lower()
229
        images = self.image_client.list_public(detail=True)['images']
230
        # Select images by `system_uuid' user
231
        images = [x for x in images if x['user_id'] == system_uuid]
232
        # Select images with `image_name' in their names
233
        images = \
234
            [x for x in images if x['name'].lower().find(image_name) != -1]
235
        # Let's select the first one
236
        return images[0]
237

    
238
    def _get_server_ip_and_port(self, server):
239
        """Compute server's IPv4 and ssh port number"""
240
        self.logger.info("Get server connection details..")
241
        # XXX: check if this IP is from public network
242
        server_ip = server['attachments'][0]['ipv4']
243
        if self.config.get('Deployment', 'deploy_on_io') == "True":
244
            tmp1 = int(server_ip.split(".")[2])
245
            tmp2 = int(server_ip.split(".")[3])
246
            server_ip = "gate.okeanos.io"
247
            server_port = 10000 + tmp1 * 256 + tmp2
248
        else:
249
            server_port = 22
250
        self.write_config('server_ip', server_ip)
251
        self.logger.debug("Server's IPv4 is %s" % _green(server_ip))
252
        self.write_config('server_port', server_port)
253
        self.logger.debug("Server's ssh port is %s" % _green(server_port))
254

    
255
    def write_config(self, option, value, section="Temporary Options"):
256
        """Write changes back to config file"""
257
        try:
258
            self.config.add_section(section)
259
        except DuplicateSectionError:
260
            pass
261
        self.config.set(section, option, str(value))
262
        temp_conf_file = self.config.get('Global', 'temporary_config')
263
        with open(temp_conf_file, 'wb') as tcf:
264
            self.config.write(tcf)
265

    
266
    def setup_fabric(self):
267
        """Setup fabric environment"""
268
        self.logger.info("Setup fabric parameters..")
269
        fabric.env.user = self.config.get('Temporary Options', 'server_user')
270
        fabric.env.host_string = \
271
            self.config.get('Temporary Options', 'server_ip')
272
        fabric.env.port = self.config.getint('Temporary Options',
273
                                             'server_port')
274
        fabric.env.password = self.config.get('Temporary Options',
275
                                              'server_passwd')
276
        fabric.env.connection_attempts = 10
277
        fabric.env.shell = "/bin/bash -c"
278
        fabric.env.disable_known_hosts = True
279
        fabric.env.output_prefix = None
280

    
281
    def _check_hash_sum(self, localfile, remotefile):
282
        """Check hash sums of two files"""
283
        self.logger.debug("Check hash sum for local file %s" % localfile)
284
        hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0]
285
        self.logger.debug("Local file has sha256 hash %s" % hash1)
286
        self.logger.debug("Check hash sum for remote file %s" % remotefile)
287
        hash2 = _run("sha256sum %s" % remotefile, False)
288
        hash2 = hash2.split(' ')[0]
289
        self.logger.debug("Remote file has sha256 hash %s" % hash2)
290
        if hash1 != hash2:
291
            self.logger.error("Hashes differ.. aborting")
292
            sys.exit(-1)
293

    
294
    @_check_fabric
295
    def clone_repo(self):
296
        """Clone Synnefo repo from slave server"""
297
        self.logger.info("Configure repositories on remote server..")
298
        self.logger.debug("Setup apt, install curl and git")
299
        cmd = """
300
        echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
301
        apt-get update
302
        apt-get install curl git --yes
303
        echo -e "\n\ndeb {0}" >> /etc/apt/sources.list
304
        curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
305
        apt-get update
306
        git config --global user.name {1}
307
        git config --global user.mail {2}
308
        """.format(self.config.get('Global', 'apt_repo'),
309
                   self.config.get('Global', 'git_config_name'),
310
                   self.config.get('Global', 'git_config_mail'))
311
        _run(cmd, False)
312

    
313
        synnefo_repo = self.config.get('Global', 'synnefo_repo')
314
        synnefo_branch = self.config.get('Global', 'synnefo_branch')
315
        # Currently clonning synnefo can fail unexpectedly
316
        cloned = False
317
        for i in range(3):
318
            self.logger.debug("Clone synnefo from %s" % synnefo_repo)
319
            cmd = ("git clone --branch %s %s"
320
                   % (synnefo_branch, synnefo_repo))
321
            try:
322
                _run(cmd, False)
323
                cloned = True
324
                break
325
            except:
326
                self.logger.warning("Clonning synnefo failed.. retrying %s"
327
                                    % i)
328
        if not cloned:
329
            self.logger.error("Can not clone Synnefo repo.")
330
            sys.exit(-1)
331

    
332
        deploy_repo = self.config.get('Global', 'deploy_repo')
333
        self.logger.debug("Clone snf-deploy from %s" % deploy_repo)
334
        _run("git clone --depth 1 %s" % deploy_repo, False)
335

    
336
    @_check_fabric
337
    def build_synnefo(self):
338
        """Build Synnefo packages"""
339
        self.logger.info("Build Synnefo packages..")
340
        self.logger.debug("Install development packages")
341
        cmd = """
342
        apt-get update
343
        apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
344
                python-dev python-all python-pip --yes
345
        pip install devflow
346
        """
347
        _run(cmd, False)
348

    
349
        if self.config.get('Global', 'patch_pydist') == "True":
350
            self.logger.debug("Patch pydist.py module")
351
            cmd = r"""
352
            sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \
353
                /usr/share/python/debpython/pydist.py
354
            """
355
            _run(cmd, False)
356

357
        self.logger.debug("Build snf-deploy package")
358
        cmd = """
359
        git checkout -t origin/debian
360
        git-buildpackage --git-upstream-branch=master \
361
                --git-debian-branch=debian \
362
                --git-export-dir=../snf-deploy_build-area \
363
                -uc -us
364
        """
365
        with fabric.cd("snf-deploy"):
366
            _run(cmd, True)
367

368
        self.logger.debug("Install snf-deploy package")
369
        cmd = """
370
        dpkg -i snf-deploy*.deb
371
        apt-get -f install --yes
372
        """
373
        with fabric.cd("snf-deploy_build-area"):
374
            with fabric.settings(warn_only=True):
375
                _run(cmd, True)
376

377
        self.logger.debug("Build synnefo packages")
378
        cmd = """
379
        devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign
380
        """
381
        with fabric.cd("synnefo"):
382
            _run(cmd, True)
383

384
        self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
385
        cmd = """
386
        cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/
387
        """
388
        _run(cmd, False)
389

390
    @_check_fabric
391
    def deploy_synnefo(self):
392
        """Deploy Synnefo using snf-deploy"""
393
        self.logger.info("Deploy Synnefo..")
394
        schema = self.config.get('Global', 'schema')
395
        schema_files = os.path.join(self.ci_dir, "schemas/%s/*" % schema)
396
        self.logger.debug("Will use %s schema" % schema)
397

398
        self.logger.debug("Upload schema files to server")
399
        with fabric.quiet():
400
            fabric.put(schema_files, "/etc/snf-deploy/")
401

402
        self.logger.debug("Change password in nodes.conf file")
403
        cmd = """
404
        sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
405
        """.format(fabric.env.password)
406
        _run(cmd, False)
407

408
        self.logger.debug("Run snf-deploy")
409
        cmd = """
410
        snf-deploy all --autoconf
411
        """
412
        _run(cmd, True)
413

414
    @_check_fabric
415
    def unit_test(self):
416
        """Run Synnefo unit test suite"""
417
        self.logger.info("Run Synnefo unit test suite")
418
        component = self.config.get('Unit Tests', 'component')
419

420
        self.logger.debug("Install needed packages")
421
        cmd = """
422
        pip install mock
423
        pip install factory_boy
424
        """
425
        _run(cmd, False)
426

427
        self.logger.debug("Upload tests.sh file")
428
        unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
429
        with fabric.quiet():
430
            fabric.put(unit_tests_file, ".")
431

432
        self.logger.debug("Run unit tests")
433
        cmd = """
434
        bash tests.sh {0}
435
        """.format(component)
436
        _run(cmd, True)
437

438
    @_check_fabric
439
    def run_burnin(self):
440
        """Run burnin functional test suite"""
441
        self.logger.info("Run Burnin functional test suite")
442
        cmd = """
443
        auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3)
444
        token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3)
445
        images_user=$(kamaki image list -l | grep owner | \
446
                      cut -d':' -f2 | tr -d ' ')
447
        snf-burnin --auth-url=$auth_url --token=$token \
448
            --force-flavor=2 --image-id=all \
449
            --system-images-user=$images_user \
450
            {0}
451
        log_folder=$(ls -1d /var/log/burnin/* | tail -n1)
452
        for i in $(ls $log_folder/*/details*); do
453
            echo -e "\\n\\n"
454
            echo -e "***** $i\\n"
455
            cat $i
456
        done
457
        """.format(self.config.get('Burnin', 'cmd_options'))
458
        _run(cmd, True)
459

460
    @_check_fabric
461
    def fetch_packages(self):
462
        """Download Synnefo packages"""
463
        self.logger.info("Download Synnefo packages")
464
        self.logger.debug("Create tarball with packages")
465
        cmd = """
466
        tar czf synnefo_build-area.tgz synnefo_build-area
467
        """
468
        _run(cmd, False)
469

470
        pkgs_dir = self.config.get('Global', 'pkgs_dir')
471
        self.logger.debug("Fetch packages to local dir %s" % pkgs_dir)
472
        os.makedirs(pkgs_dir)
473
        with fabric.quiet():
474
            fabric.get("synnefo_build-area.tgz", pkgs_dir)
475

476
        pkgs_file = os.path.join(pkgs_dir, "synnefo_build-area.tgz")
477
        self._check_hash_sum(pkgs_file, "synnefo_build-area.tgz")
478

479
        self.logger.debug("Untar packages file %s" % pkgs_file)
480
        os.system("cd %s; tar xzf synnefo_build-area.tgz" % pkgs_dir)
481