root / ci / utils.py @ e5d2788b
History | View | Annotate | Download (27 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 |
|
23 |
DEFAULT_CONFIG_FILE = "new_config"
|
24 |
# UUID of owner of system images
|
25 |
DEFAULT_SYSTEM_IMAGES_UUID = [ |
26 |
"25ecced9-bf53-4145-91ee-cf47377e9fb2", # production (okeanos.grnet.gr) |
27 |
"04cbe33f-29b7-4ef1-94fb-015929e5fc06", # testing (okeanos.io) |
28 |
] |
29 |
|
30 |
|
31 |
def _run(cmd, verbose): |
32 |
"""Run fabric with verbose level"""
|
33 |
if verbose:
|
34 |
args = ('running',)
|
35 |
else:
|
36 |
args = ('running', 'stdout',) |
37 |
with fabric.hide(*args): # Used * or ** magic. pylint: disable-msg=W0142 |
38 |
return fabric.run(cmd)
|
39 |
|
40 |
|
41 |
def _put(local, remote): |
42 |
"""Run fabric put command without output"""
|
43 |
with fabric.quiet():
|
44 |
fabric.put(local, remote) |
45 |
|
46 |
|
47 |
def _red(msg): |
48 |
"""Red color"""
|
49 |
#return "\x1b[31m" + str(msg) + "\x1b[0m"
|
50 |
return str(msg) |
51 |
|
52 |
|
53 |
def _yellow(msg): |
54 |
"""Yellow color"""
|
55 |
#return "\x1b[33m" + str(msg) + "\x1b[0m"
|
56 |
return str(msg) |
57 |
|
58 |
|
59 |
def _green(msg): |
60 |
"""Green color"""
|
61 |
#return "\x1b[32m" + str(msg) + "\x1b[0m"
|
62 |
return str(msg) |
63 |
|
64 |
|
65 |
def _check_fabric(fun): |
66 |
"""Check if fabric env has been set"""
|
67 |
def wrapper(self, *args, **kwargs): |
68 |
"""wrapper function"""
|
69 |
if not self.fabric_installed: |
70 |
self.setup_fabric()
|
71 |
self.fabric_installed = True |
72 |
return fun(self, *args, **kwargs) |
73 |
return wrapper
|
74 |
|
75 |
|
76 |
def _check_kamaki(fun): |
77 |
"""Check if kamaki has been initialized"""
|
78 |
def wrapper(self, *args, **kwargs): |
79 |
"""wrapper function"""
|
80 |
if not self.kamaki_installed: |
81 |
self.setup_kamaki()
|
82 |
self.kamaki_installed = True |
83 |
return fun(self, *args, **kwargs) |
84 |
return wrapper
|
85 |
|
86 |
|
87 |
class _MyFormatter(logging.Formatter): |
88 |
"""Logging Formatter"""
|
89 |
def format(self, record): |
90 |
format_orig = self._fmt
|
91 |
if record.levelno == logging.DEBUG:
|
92 |
self._fmt = " %(msg)s" |
93 |
elif record.levelno == logging.INFO:
|
94 |
self._fmt = "%(msg)s" |
95 |
elif record.levelno == logging.WARNING:
|
96 |
self._fmt = _yellow("[W] %(msg)s") |
97 |
elif record.levelno == logging.ERROR:
|
98 |
self._fmt = _red("[E] %(msg)s") |
99 |
result = logging.Formatter.format(self, record)
|
100 |
self._fmt = format_orig
|
101 |
return result
|
102 |
|
103 |
|
104 |
# Too few public methods. pylint: disable-msg=R0903
|
105 |
class _InfoFilter(logging.Filter): |
106 |
"""Logging Filter that allows DEBUG and INFO messages only"""
|
107 |
def filter(self, rec): |
108 |
"""The filter"""
|
109 |
return rec.levelno in (logging.DEBUG, logging.INFO) |
110 |
|
111 |
|
112 |
# Too many instance attributes. pylint: disable-msg=R0902
|
113 |
class SynnefoCI(object): |
114 |
"""SynnefoCI python class"""
|
115 |
|
116 |
def __init__(self, config_file=None, build_id=None, cloud=None): |
117 |
""" Initialize SynnefoCI python class
|
118 |
|
119 |
Setup logger, local_dir, config and kamaki
|
120 |
"""
|
121 |
# Setup logger
|
122 |
self.logger = logging.getLogger('synnefo-ci') |
123 |
self.logger.setLevel(logging.DEBUG)
|
124 |
|
125 |
handler1 = logging.StreamHandler(sys.stdout) |
126 |
handler1.setLevel(logging.DEBUG) |
127 |
handler1.addFilter(_InfoFilter()) |
128 |
handler1.setFormatter(_MyFormatter()) |
129 |
handler2 = logging.StreamHandler(sys.stderr) |
130 |
handler2.setLevel(logging.WARNING) |
131 |
handler2.setFormatter(_MyFormatter()) |
132 |
|
133 |
self.logger.addHandler(handler1)
|
134 |
self.logger.addHandler(handler2)
|
135 |
|
136 |
# Get our local dir
|
137 |
self.ci_dir = os.path.dirname(os.path.abspath(__file__))
|
138 |
self.repo_dir = os.path.dirname(self.ci_dir) |
139 |
|
140 |
# Read config file
|
141 |
if config_file is None: |
142 |
config_file = DEFAULT_CONFIG_FILE |
143 |
if not os.path.isabs(config_file): |
144 |
config_file = os.path.join(self.ci_dir, config_file)
|
145 |
self.config = ConfigParser()
|
146 |
self.config.optionxform = str |
147 |
self.config.read(config_file)
|
148 |
|
149 |
# Read temporary_config file
|
150 |
temp_config = self.config.get('Global', 'temporary_config') |
151 |
self.temp_config = ConfigParser()
|
152 |
self.temp_config.optionxform = str |
153 |
self.temp_config.read(temp_config)
|
154 |
self.build_id = build_id
|
155 |
self.logger.info("Will use \"%s\" as build id" % _green(self.build_id)) |
156 |
|
157 |
# Set kamaki cloud
|
158 |
if cloud is not None: |
159 |
self.kamaki_cloud = cloud
|
160 |
elif self.config.has_option("Deployment", "kamaki_cloud"): |
161 |
kamaki_cloud = self.config.get("Deployment", "kamaki_cloud") |
162 |
if kamaki_cloud == "": |
163 |
self.kamaki_cloud = None |
164 |
else:
|
165 |
self.kamaki_cloud = None |
166 |
|
167 |
# Initialize variables
|
168 |
self.fabric_installed = False |
169 |
self.kamaki_installed = False |
170 |
self.cyclades_client = None |
171 |
self.compute_client = None |
172 |
self.image_client = None |
173 |
|
174 |
def setup_kamaki(self): |
175 |
"""Initialize kamaki
|
176 |
|
177 |
Setup cyclades_client, image_client and compute_client
|
178 |
"""
|
179 |
|
180 |
config = kamaki_config.Config() |
181 |
if self.kamaki_cloud is None: |
182 |
self.kamaki_cloud = config.get_global("default_cloud") |
183 |
|
184 |
self.logger.info("Setup kamaki client, using cloud '%s'.." % |
185 |
self.kamaki_cloud)
|
186 |
auth_url = config.get_cloud(self.kamaki_cloud, "url") |
187 |
self.logger.debug("Authentication URL is %s" % _green(auth_url)) |
188 |
token = config.get_cloud(self.kamaki_cloud, "token") |
189 |
#self.logger.debug("Token is %s" % _green(token))
|
190 |
|
191 |
astakos_client = AstakosClient(auth_url, token) |
192 |
|
193 |
cyclades_url = \ |
194 |
astakos_client.get_service_endpoints('compute')['publicURL'] |
195 |
self.logger.debug("Cyclades API url is %s" % _green(cyclades_url)) |
196 |
self.cyclades_client = CycladesClient(cyclades_url, token)
|
197 |
self.cyclades_client.CONNECTION_RETRY_LIMIT = 2 |
198 |
|
199 |
image_url = \ |
200 |
astakos_client.get_service_endpoints('image')['publicURL'] |
201 |
self.logger.debug("Images API url is %s" % _green(image_url)) |
202 |
self.image_client = ImageClient(cyclades_url, token)
|
203 |
self.image_client.CONNECTION_RETRY_LIMIT = 2 |
204 |
|
205 |
compute_url = \ |
206 |
astakos_client.get_service_endpoints('compute')['publicURL'] |
207 |
self.logger.debug("Compute API url is %s" % _green(compute_url)) |
208 |
self.compute_client = ComputeClient(compute_url, token)
|
209 |
self.compute_client.CONNECTION_RETRY_LIMIT = 2 |
210 |
|
211 |
def _wait_transition(self, server_id, current_status, new_status): |
212 |
"""Wait for server to go from current_status to new_status"""
|
213 |
self.logger.debug("Waiting for server to become %s" % new_status) |
214 |
timeout = self.config.getint('Global', 'build_timeout') |
215 |
sleep_time = 5
|
216 |
while True: |
217 |
server = self.cyclades_client.get_server_details(server_id)
|
218 |
if server['status'] == new_status: |
219 |
return server
|
220 |
elif timeout < 0: |
221 |
self.logger.error(
|
222 |
"Waiting for server to become %s timed out" % new_status)
|
223 |
self.destroy_server(False) |
224 |
sys.exit(-1)
|
225 |
elif server['status'] == current_status: |
226 |
# Sleep for #n secs and continue
|
227 |
timeout = timeout - sleep_time |
228 |
time.sleep(sleep_time) |
229 |
else:
|
230 |
self.logger.error(
|
231 |
"Server failed with status %s" % server['status']) |
232 |
self.destroy_server(False) |
233 |
sys.exit(-1)
|
234 |
|
235 |
@_check_kamaki
|
236 |
def destroy_server(self, wait=True): |
237 |
"""Destroy slave server"""
|
238 |
server_id = int(self.read_temp_config('server_id')) |
239 |
self.logger.info("Destoying server with id %s " % server_id) |
240 |
self.cyclades_client.delete_server(server_id)
|
241 |
if wait:
|
242 |
self._wait_transition(server_id, "ACTIVE", "DELETED") |
243 |
|
244 |
@_check_kamaki
|
245 |
def create_server(self, image_id=None, flavor_name=None, ssh_keys=None): |
246 |
"""Create slave server"""
|
247 |
self.logger.info("Create a new server..") |
248 |
|
249 |
# Find a build_id to use
|
250 |
if self.build_id is None: |
251 |
# If build_id is given use this, else ..
|
252 |
# Find a uniq build_id to use
|
253 |
ids = self.temp_config.sections()
|
254 |
if ids:
|
255 |
max_id = int(max(self.temp_config.sections(), key=int)) |
256 |
self.build_id = max_id + 1 |
257 |
else:
|
258 |
self.build_id = 1 |
259 |
self.logger.debug("New build id \"%s\" was created" |
260 |
% _green(self.build_id))
|
261 |
|
262 |
# Find an image to use
|
263 |
if image_id is None: |
264 |
image = self._find_image()
|
265 |
self.logger.debug("Will use image \"%s\"" % _green(image['name'])) |
266 |
image_id = image["id"]
|
267 |
self.logger.debug("Image has id %s" % _green(image_id)) |
268 |
# Find a flavor to use
|
269 |
flavor_id = self._find_flavor(flavor_name)
|
270 |
server = self.cyclades_client.create_server(
|
271 |
self.config.get('Deployment', 'server_name'), |
272 |
flavor_id, |
273 |
image_id) |
274 |
server_id = server['id']
|
275 |
self.write_temp_config('server_id', server_id) |
276 |
self.logger.debug("Server got id %s" % _green(server_id)) |
277 |
server_user = server['metadata']['users'] |
278 |
self.write_temp_config('server_user', server_user) |
279 |
self.logger.debug("Server's admin user is %s" % _green(server_user)) |
280 |
server_passwd = server['adminPass']
|
281 |
self.write_temp_config('server_passwd', server_passwd) |
282 |
|
283 |
server = self._wait_transition(server_id, "BUILD", "ACTIVE") |
284 |
self._get_server_ip_and_port(server)
|
285 |
self._copy_ssh_keys(ssh_keys)
|
286 |
|
287 |
# Setup Firewall
|
288 |
self.setup_fabric()
|
289 |
self.logger.info("Setup firewall") |
290 |
accept_ssh_from = self.config.get('Global', 'accept_ssh_from') |
291 |
if accept_ssh_from != "": |
292 |
self.logger.debug("Block ssh except from %s" % accept_ssh_from) |
293 |
cmd = """
|
294 |
local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
|
295 |
cut -d':' -f2 | cut -d' ' -f1)
|
296 |
iptables -A INPUT -s localhost -j ACCEPT
|
297 |
iptables -A INPUT -s $local_ip -j ACCEPT
|
298 |
iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
|
299 |
iptables -A INPUT -p tcp --dport 22 -j DROP
|
300 |
""".format(accept_ssh_from)
|
301 |
_run(cmd, False)
|
302 |
|
303 |
# Setup apt, download packages
|
304 |
self.logger.debug("Setup apt. Install x2goserver and firefox") |
305 |
cmd = """
|
306 |
echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
|
307 |
apt-get update
|
308 |
apt-get install curl --yes
|
309 |
echo -e "\n\n{0}" >> /etc/apt/sources.list
|
310 |
# Synnefo repo's key
|
311 |
curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
|
312 |
# X2GO Key
|
313 |
apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
|
314 |
apt-get install x2go-keyring
|
315 |
apt-get update
|
316 |
apt-get install x2goserver x2goserver-xsession iceweasel
|
317 |
""".format(self.config.get('Global', 'apt_repo')) |
318 |
_run(cmd, False)
|
319 |
|
320 |
def _find_flavor(self, flavor_name): |
321 |
"""Given a flavor_name (reg expression) find a flavor id to use"""
|
322 |
# Get a list of flavor names from config file
|
323 |
flavor_names = self.config.get('Deployment', 'flavor_name').split(",") |
324 |
if flavor_name is not None: |
325 |
# If we have a flavor_name to use, add it to our list
|
326 |
flavor_names.insert(0, flavor_name)
|
327 |
|
328 |
flavors = self.compute_client.list_flavors()
|
329 |
for flname in flavor_names: |
330 |
sflname = flname.strip() |
331 |
self.logger.debug("Try to find a flavor with name \"%s\"" % sflname) |
332 |
fls = [f for f in flavors |
333 |
if re.search(sflname, f['name']) is not None] |
334 |
if fls:
|
335 |
self.logger.debug("Will use %s with id %s" |
336 |
% (fls[0]['name'], fls[0]['id'])) |
337 |
return fls[0]['id'] |
338 |
|
339 |
self.logger.error("No matching flavor found.. aborting") |
340 |
sys.exit(1)
|
341 |
|
342 |
def _find_image(self): |
343 |
"""Find a suitable image to use
|
344 |
|
345 |
It has to belong to one of the `DEFAULT_SYSTEM_IMAGES_UUID'
|
346 |
users and contain the word given by `image_name' option.
|
347 |
"""
|
348 |
image_name = self.config.get('Deployment', 'image_name').lower() |
349 |
images = self.image_client.list_public(detail=True)['images'] |
350 |
# Select images by `system_uuid' user
|
351 |
images = [x for x in images |
352 |
if x['user_id'] in DEFAULT_SYSTEM_IMAGES_UUID] |
353 |
# Select images with `image_name' in their names
|
354 |
images = [x for x in images |
355 |
if x['name'].lower().find(image_name) != -1] |
356 |
# Let's select the first one
|
357 |
return images[0] |
358 |
|
359 |
def _get_server_ip_and_port(self, server): |
360 |
"""Compute server's IPv4 and ssh port number"""
|
361 |
self.logger.info("Get server connection details..") |
362 |
server_ip = server['attachments'][0]['ipv4'] |
363 |
if ".okeanos.io" in self.cyclades_client.base_url: |
364 |
tmp1 = int(server_ip.split(".")[2]) |
365 |
tmp2 = int(server_ip.split(".")[3]) |
366 |
server_ip = "gate.okeanos.io"
|
367 |
server_port = 10000 + tmp1 * 256 + tmp2 |
368 |
else:
|
369 |
server_port = 22
|
370 |
self.write_temp_config('server_ip', server_ip) |
371 |
self.logger.debug("Server's IPv4 is %s" % _green(server_ip)) |
372 |
self.write_temp_config('server_port', server_port) |
373 |
self.logger.debug("Server's ssh port is %s" % _green(server_port)) |
374 |
self.logger.debug("Access server using \"ssh -X -p %s %s@%s\"" % |
375 |
(server_port, server['metadata']['users'], server_ip)) |
376 |
|
377 |
@_check_fabric
|
378 |
def _copy_ssh_keys(self, ssh_keys): |
379 |
"""Upload/Install ssh keys to server"""
|
380 |
self.logger.debug("Check for authentication keys to use") |
381 |
if ssh_keys is None: |
382 |
ssh_keys = self.config.get("Deployment", "ssh_keys") |
383 |
|
384 |
if ssh_keys != "": |
385 |
self.logger.debug("Will use %s authentication keys file" % ssh_keys) |
386 |
keyfile = '/tmp/%s.pub' % fabric.env.user
|
387 |
_run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False) |
388 |
if ssh_keys.startswith("http://") or \ |
389 |
ssh_keys.startswith("https://") or \ |
390 |
ssh_keys.startswith("ftp://"):
|
391 |
cmd = """
|
392 |
apt-get update
|
393 |
apt-get install wget --yes
|
394 |
wget {0} -O {1} --no-check-certificate
|
395 |
""".format(ssh_keys, keyfile)
|
396 |
_run(cmd, False)
|
397 |
elif os.path.exists(ssh_keys):
|
398 |
_put(ssh_keys, keyfile) |
399 |
else:
|
400 |
self.logger.debug("No ssh keys found") |
401 |
_run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False) |
402 |
_run('rm %s' % keyfile, False) |
403 |
self.logger.debug("Uploaded ssh authorized keys") |
404 |
else:
|
405 |
self.logger.debug("No ssh keys found") |
406 |
|
407 |
def write_temp_config(self, option, value): |
408 |
"""Write changes back to config file"""
|
409 |
# If build_id section doesn't exist create a new one
|
410 |
try:
|
411 |
self.temp_config.add_section(str(self.build_id)) |
412 |
creation_time = time.strftime("%a, %d %b %Y %X", time.localtime())
|
413 |
self.write_temp_config("created", creation_time) |
414 |
except DuplicateSectionError:
|
415 |
pass
|
416 |
self.temp_config.set(str(self.build_id), option, str(value)) |
417 |
curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
|
418 |
self.temp_config.set(str(self.build_id), "modified", curr_time) |
419 |
temp_conf_file = self.config.get('Global', 'temporary_config') |
420 |
with open(temp_conf_file, 'wb') as tcf: |
421 |
self.temp_config.write(tcf)
|
422 |
|
423 |
def read_temp_config(self, option): |
424 |
"""Read from temporary_config file"""
|
425 |
# If build_id is None use the latest one
|
426 |
if self.build_id is None: |
427 |
ids = self.temp_config.sections()
|
428 |
if ids:
|
429 |
self.build_id = int(ids[-1]) |
430 |
else:
|
431 |
self.logger.error("No sections in temporary config file") |
432 |
sys.exit(1)
|
433 |
self.logger.debug("Will use \"%s\" as build id" |
434 |
% _green(self.build_id))
|
435 |
# Read specified option
|
436 |
return self.temp_config.get(str(self.build_id), option) |
437 |
|
438 |
def setup_fabric(self): |
439 |
"""Setup fabric environment"""
|
440 |
self.logger.info("Setup fabric parameters..") |
441 |
fabric.env.user = self.read_temp_config('server_user') |
442 |
fabric.env.host_string = self.read_temp_config('server_ip') |
443 |
fabric.env.port = int(self.read_temp_config('server_port')) |
444 |
fabric.env.password = self.read_temp_config('server_passwd') |
445 |
fabric.env.connection_attempts = 10
|
446 |
fabric.env.shell = "/bin/bash -c"
|
447 |
fabric.env.disable_known_hosts = True
|
448 |
fabric.env.output_prefix = None
|
449 |
|
450 |
def _check_hash_sum(self, localfile, remotefile): |
451 |
"""Check hash sums of two files"""
|
452 |
self.logger.debug("Check hash sum for local file %s" % localfile) |
453 |
hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0] |
454 |
self.logger.debug("Local file has sha256 hash %s" % hash1) |
455 |
self.logger.debug("Check hash sum for remote file %s" % remotefile) |
456 |
hash2 = _run("sha256sum %s" % remotefile, False) |
457 |
hash2 = hash2.split(' ')[0] |
458 |
self.logger.debug("Remote file has sha256 hash %s" % hash2) |
459 |
if hash1 != hash2:
|
460 |
self.logger.error("Hashes differ.. aborting") |
461 |
sys.exit(-1)
|
462 |
|
463 |
@_check_fabric
|
464 |
def clone_repo(self, local_repo=False): |
465 |
"""Clone Synnefo repo from slave server"""
|
466 |
self.logger.info("Configure repositories on remote server..") |
467 |
self.logger.debug("Install/Setup git") |
468 |
cmd = """
|
469 |
apt-get install git --yes
|
470 |
git config --global user.name {0}
|
471 |
git config --global user.email {1}
|
472 |
""".format(self.config.get('Global', 'git_config_name'), |
473 |
self.config.get('Global', 'git_config_mail')) |
474 |
_run(cmd, False)
|
475 |
|
476 |
# Find synnefo_repo and synnefo_branch to use
|
477 |
synnefo_repo = self.config.get('Global', 'synnefo_repo') |
478 |
synnefo_branch = self.config.get("Global", "synnefo_branch") |
479 |
if synnefo_branch == "": |
480 |
synnefo_branch = \ |
481 |
subprocess.Popen( |
482 |
["git", "rev-parse", "--abbrev-ref", "HEAD"], |
483 |
stdout=subprocess.PIPE).communicate()[0].strip()
|
484 |
if synnefo_branch == "HEAD": |
485 |
synnefo_branch = \ |
486 |
subprocess.Popen( |
487 |
["git", "rev-parse", "--short", "HEAD"], |
488 |
stdout=subprocess.PIPE).communicate()[0].strip()
|
489 |
self.logger.info("Will use branch %s" % synnefo_branch) |
490 |
|
491 |
if local_repo or synnefo_branch == "": |
492 |
# Use local_repo
|
493 |
self.logger.debug("Push local repo to server") |
494 |
# Firstly create the remote repo
|
495 |
_run("git init synnefo", False) |
496 |
# Then push our local repo over ssh
|
497 |
# We have to pass some arguments to ssh command
|
498 |
# namely to disable host checking.
|
499 |
(temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp() |
500 |
os.close(temp_ssh_file_handle) |
501 |
cmd = """
|
502 |
echo 'exec ssh -o "StrictHostKeyChecking no" \
|
503 |
-o "UserKnownHostsFile /dev/null" \
|
504 |
-q "$@"' > {4}
|
505 |
chmod u+x {4}
|
506 |
export GIT_SSH="{4}"
|
507 |
echo "{0}" | git push --mirror ssh://{1}@{2}:{3}/~/synnefo
|
508 |
rm -f {4}
|
509 |
""".format(fabric.env.password,
|
510 |
fabric.env.user, |
511 |
fabric.env.host_string, |
512 |
fabric.env.port, |
513 |
temp_ssh_file) |
514 |
os.system(cmd) |
515 |
else:
|
516 |
# Clone Synnefo from remote repo
|
517 |
# Currently clonning synnefo can fail unexpectedly
|
518 |
cloned = False
|
519 |
for i in range(10): |
520 |
self.logger.debug("Clone synnefo from %s" % synnefo_repo) |
521 |
try:
|
522 |
_run("git clone %s synnefo" % synnefo_repo, False) |
523 |
cloned = True
|
524 |
break
|
525 |
except BaseException: |
526 |
self.logger.warning(
|
527 |
"Clonning synnefo failed.. retrying %s" % i)
|
528 |
if not cloned: |
529 |
self.logger.error("Can not clone Synnefo repo.") |
530 |
sys.exit(-1)
|
531 |
|
532 |
# Checkout the desired synnefo_branch
|
533 |
self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch) |
534 |
cmd = """
|
535 |
cd synnefo
|
536 |
for branch in `git branch -a | grep remotes | \
|
537 |
grep -v HEAD | grep -v master`; do
|
538 |
git branch --track ${branch##*/} $branch
|
539 |
done
|
540 |
git checkout %s
|
541 |
""" % (synnefo_branch)
|
542 |
_run(cmd, False)
|
543 |
|
544 |
@_check_fabric
|
545 |
def build_synnefo(self): |
546 |
"""Build Synnefo packages"""
|
547 |
self.logger.info("Build Synnefo packages..") |
548 |
self.logger.debug("Install development packages") |
549 |
cmd = """
|
550 |
apt-get update
|
551 |
apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
|
552 |
python-dev python-all python-pip --yes
|
553 |
pip install devflow
|
554 |
"""
|
555 |
_run(cmd, False)
|
556 |
|
557 |
if self.config.get('Global', 'patch_pydist') == "True": |
558 |
self.logger.debug("Patch pydist.py module") |
559 |
cmd = r"""
|
560 |
sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \ |
561 |
/usr/share/python/debpython/pydist.py |
562 |
"""
|
563 |
_run(cmd, False)
|
564 |
|
565 |
# Build synnefo packages
|
566 |
self.logger.debug("Build synnefo packages")
|
567 |
cmd = """
|
568 |
devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign |
569 |
"""
|
570 |
with fabric.cd("synnefo"):
|
571 |
_run(cmd, True)
|
572 |
|
573 |
# Install snf-deploy package
|
574 |
self.logger.debug("Install snf-deploy package")
|
575 |
cmd = """
|
576 |
dpkg -i snf-deploy*.deb |
577 |
apt-get -f install --yes |
578 |
"""
|
579 |
with fabric.cd("synnefo_build-area"):
|
580 |
with fabric.settings(warn_only=True):
|
581 |
_run(cmd, True)
|
582 |
|
583 |
# Setup synnefo packages for snf-deploy
|
584 |
self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
|
585 |
cmd = """
|
586 |
cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/ |
587 |
"""
|
588 |
_run(cmd, False)
|
589 |
|
590 |
@_check_fabric
|
591 |
def build_documentation(self):
|
592 |
"""Build Synnefo documentation""" |
593 |
self.logger.info("Build Synnefo documentation..")
|
594 |
_run("pip install -U Sphinx", False)
|
595 |
with fabric.cd("synnefo"):
|
596 |
_run("devflow-update-version; "
|
597 |
"./ci/make_docs.sh synnefo_documentation", False)
|
598 |
|
599 |
def fetch_documentation(self, dest=None):
|
600 |
"""Fetch Synnefo documentation""" |
601 |
self.logger.info("Fetch Synnefo documentation..")
|
602 |
if dest is None:
|
603 |
dest = "synnefo_documentation"
|
604 |
dest = os.path.abspath(dest)
|
605 |
if not os.path.exists(dest):
|
606 |
os.makedirs(dest)
|
607 |
self.fetch_compressed("synnefo/synnefo_documentation", dest)
|
608 |
self.logger.info("Downloaded documentation to %s" %
|
609 |
_green(dest))
|
610 |
|
611 |
@_check_fabric
|
612 |
def deploy_synnefo(self, schema=None):
|
613 |
"""Deploy Synnefo using snf-deploy""" |
614 |
self.logger.info("Deploy Synnefo..")
|
615 |
if schema is None:
|
616 |
schema = self.config.get('Global', 'schema')
|
617 |
self.logger.debug("Will use %s schema" % schema)
|
618 |
|
619 |
schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
|
620 |
if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
|
621 |
raise ValueError("Unknown schema: %s" % schema)
|
622 |
|
623 |
self.logger.debug("Upload schema files to server")
|
624 |
_put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/")
|
625 |
|
626 |
self.logger.debug("Change password in nodes.conf file")
|
627 |
cmd = """
|
628 |
sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
|
629 |
""".format(fabric.env.password)
|
630 |
_run(cmd, False)
|
631 |
|
632 |
self.logger.debug("Run snf-deploy")
|
633 |
cmd = """
|
634 |
snf-deploy --disable-colors --autoconf all
|
635 |
"""
|
636 |
_run(cmd, True)
|
637 |
|
638 |
@_check_fabric
|
639 |
def unit_test(self):
|
640 |
"""Run Synnefo unit test suite""" |
641 |
self.logger.info("Run Synnefo unit test suite")
|
642 |
component = self.config.get('Unit Tests', 'component')
|
643 |
|
644 |
self.logger.debug("Install needed packages")
|
645 |
cmd = """
|
646 |
pip install mock |
647 |
pip install factory_boy |
648 |
"""
|
649 |
_run(cmd, False)
|
650 |
|
651 |
self.logger.debug("Upload tests.sh file")
|
652 |
unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
|
653 |
_put(unit_tests_file, ".")
|
654 |
|
655 |
self.logger.debug("Run unit tests")
|
656 |
cmd = """
|
657 |
bash tests.sh {0}
|
658 |
""".format(component)
|
659 |
_run(cmd, True)
|
660 |
|
661 |
@_check_fabric
|
662 |
def run_burnin(self):
|
663 |
"""Run burnin functional test suite""" |
664 |
self.logger.info("Run Burnin functional test suite")
|
665 |
cmd = """
|
666 |
auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3) |
667 |
token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3) |
668 |
images_user=$(kamaki image list -l | grep owner | \ |
669 |
cut -d':' -f2 | tr -d ' ') |
670 |
snf-burnin --auth-url=$auth_url --token=$token \ |
671 |
--force-flavor=2 --image-id=all \ |
672 |
--system-images-user=$images_user \
|
673 |
{0}
|
674 |
log_folder=$(ls -1d /var/log/burnin/* | tail -n1) |
675 |
for i in $(ls $log_folder/*/details*); do |
676 |
echo -e "\\n\\n"
|
677 |
echo -e "***** $i\\n"
|
678 |
cat $i
|
679 |
done |
680 |
""".format(self.config.get('Burnin', 'cmd_options'))
|
681 |
_run(cmd, True)
|
682 |
|
683 |
@_check_fabric
|
684 |
def fetch_compressed(self, src, dest=None):
|
685 |
"""Create a tarball and fetch it locally""" |
686 |
self.logger.debug("Creating tarball of %s" % src)
|
687 |
basename = os.path.basename(src)
|
688 |
tar_file = basename + ".tgz"
|
689 |
cmd = "tar czf %s %s" % (tar_file, src)
|
690 |
_run(cmd, False)
|
691 |
if not os.path.exists(dest):
|
692 |
os.makedirs(dest)
|
693 |
|
694 |
tmp_dir = tempfile.mkdtemp()
|
695 |
fabric.get(tar_file, tmp_dir)
|
696 |
|
697 |
dest_file = os.path.join(tmp_dir, tar_file)
|
698 |
self._check_hash_sum(dest_file, tar_file)
|
699 |
self.logger.debug("Untar packages file %s" % dest_file)
|
700 |
cmd = """
|
701 |
cd %s |
702 |
tar xzf %s |
703 |
cp -r %s/* %s |
704 |
rm -r %s |
705 |
""" % (tmp_dir, tar_file, src, dest, tmp_dir)
|
706 |
os.system(cmd)
|
707 |
self.logger.info("Downloaded %s to %s" %
|
708 |
(src, _green(dest)))
|
709 |
|
710 |
@_check_fabric
|
711 |
def fetch_packages(self, dest=None):
|
712 |
"""Fetch Synnefo packages""" |
713 |
if dest is None:
|
714 |
dest = self.config.get('Global', 'pkgs_dir')
|
715 |
dest = os.path.abspath(dest)
|
716 |
if not os.path.exists(dest):
|
717 |
os.makedirs(dest)
|
718 |
self.fetch_compressed("synnefo_build-area", dest)
|
719 |
self.logger.info("Downloaded debian packages to %s" %
|
720 |
_green(dest))
|
721 |
|