root / ci / utils.py @ 49fb00cc
History | View | Annotate | Download (40.6 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, parse_endpoints |
19 |
from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient |
20 |
from kamaki.clients.image import ImageClient |
21 |
from kamaki.clients.compute import ComputeClient |
22 |
from kamaki.clients import ClientError |
23 |
import filelocker |
24 |
|
25 |
DEFAULT_CONFIG_FILE = "ci_wheezy.conf"
|
26 |
# Is our terminal a colorful one?
|
27 |
USE_COLORS = True
|
28 |
# UUID of owner of system images
|
29 |
DEFAULT_SYSTEM_IMAGES_UUID = [ |
30 |
"25ecced9-bf53-4145-91ee-cf47377e9fb2", # production (okeanos.grnet.gr) |
31 |
"04cbe33f-29b7-4ef1-94fb-015929e5fc06", # testing (okeanos.io) |
32 |
] |
33 |
|
34 |
|
35 |
def _run(cmd, verbose): |
36 |
"""Run fabric with verbose level"""
|
37 |
if verbose:
|
38 |
args = ('running',)
|
39 |
else:
|
40 |
args = ('running', 'stdout',) |
41 |
with fabric.hide(*args): # Used * or ** magic. pylint: disable-msg=W0142 |
42 |
return fabric.run(cmd)
|
43 |
|
44 |
|
45 |
def _put(local, remote): |
46 |
"""Run fabric put command without output"""
|
47 |
with fabric.quiet():
|
48 |
fabric.put(local, remote) |
49 |
|
50 |
|
51 |
def _red(msg): |
52 |
"""Red color"""
|
53 |
ret = "\x1b[31m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg) |
54 |
return ret
|
55 |
|
56 |
|
57 |
def _yellow(msg): |
58 |
"""Yellow color"""
|
59 |
ret = "\x1b[33m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg) |
60 |
return ret
|
61 |
|
62 |
|
63 |
def _green(msg): |
64 |
"""Green color"""
|
65 |
ret = "\x1b[32m" + str(msg) + "\x1b[0m" if USE_COLORS else str(msg) |
66 |
return ret
|
67 |
|
68 |
|
69 |
def _check_fabric(fun): |
70 |
"""Check if fabric env has been set"""
|
71 |
def wrapper(self, *args, **kwargs): |
72 |
"""wrapper function"""
|
73 |
if not self.fabric_installed: |
74 |
self.setup_fabric()
|
75 |
self.fabric_installed = True |
76 |
return fun(self, *args, **kwargs) |
77 |
return wrapper
|
78 |
|
79 |
|
80 |
def _check_kamaki(fun): |
81 |
"""Check if kamaki has been initialized"""
|
82 |
def wrapper(self, *args, **kwargs): |
83 |
"""wrapper function"""
|
84 |
if not self.kamaki_installed: |
85 |
self.setup_kamaki()
|
86 |
self.kamaki_installed = True |
87 |
return fun(self, *args, **kwargs) |
88 |
return wrapper
|
89 |
|
90 |
|
91 |
class _MyFormatter(logging.Formatter): |
92 |
"""Logging Formatter"""
|
93 |
def format(self, record): |
94 |
format_orig = self._fmt
|
95 |
if record.levelno == logging.DEBUG:
|
96 |
self._fmt = " %(message)s" |
97 |
elif record.levelno == logging.INFO:
|
98 |
self._fmt = "%(message)s" |
99 |
elif record.levelno == logging.WARNING:
|
100 |
self._fmt = _yellow("[W] %(message)s") |
101 |
elif record.levelno == logging.ERROR:
|
102 |
self._fmt = _red("[E] %(message)s") |
103 |
result = logging.Formatter.format(self, record)
|
104 |
self._fmt = format_orig
|
105 |
return result
|
106 |
|
107 |
|
108 |
# Too few public methods. pylint: disable-msg=R0903
|
109 |
class _InfoFilter(logging.Filter): |
110 |
"""Logging Filter that allows DEBUG and INFO messages only"""
|
111 |
def filter(self, rec): |
112 |
"""The filter"""
|
113 |
return rec.levelno in (logging.DEBUG, logging.INFO) |
114 |
|
115 |
|
116 |
# Too many instance attributes. pylint: disable-msg=R0902
|
117 |
class SynnefoCI(object): |
118 |
"""SynnefoCI python class"""
|
119 |
|
120 |
def __init__(self, config_file=None, build_id=None, cloud=None): |
121 |
""" Initialize SynnefoCI python class
|
122 |
|
123 |
Setup logger, local_dir, config and kamaki
|
124 |
"""
|
125 |
# Setup logger
|
126 |
self.logger = logging.getLogger('synnefo-ci') |
127 |
self.logger.setLevel(logging.DEBUG)
|
128 |
|
129 |
handler1 = logging.StreamHandler(sys.stdout) |
130 |
handler1.setLevel(logging.DEBUG) |
131 |
handler1.addFilter(_InfoFilter()) |
132 |
handler1.setFormatter(_MyFormatter()) |
133 |
handler2 = logging.StreamHandler(sys.stderr) |
134 |
handler2.setLevel(logging.WARNING) |
135 |
handler2.setFormatter(_MyFormatter()) |
136 |
|
137 |
self.logger.addHandler(handler1)
|
138 |
self.logger.addHandler(handler2)
|
139 |
|
140 |
# Get our local dir
|
141 |
self.ci_dir = os.path.dirname(os.path.abspath(__file__))
|
142 |
self.repo_dir = os.path.dirname(self.ci_dir) |
143 |
|
144 |
# Read config file
|
145 |
if config_file is None: |
146 |
config_file = os.path.join(self.ci_dir, DEFAULT_CONFIG_FILE)
|
147 |
config_file = os.path.abspath(config_file) |
148 |
self.config = ConfigParser()
|
149 |
self.config.optionxform = str |
150 |
self.config.read(config_file)
|
151 |
|
152 |
# Read temporary_config file
|
153 |
self.temp_config_file = \
|
154 |
os.path.expanduser(self.config.get('Global', 'temporary_config')) |
155 |
self.temp_config = ConfigParser()
|
156 |
self.temp_config.optionxform = str |
157 |
self.temp_config.read(self.temp_config_file) |
158 |
self.build_id = build_id
|
159 |
if build_id is not None: |
160 |
self.logger.info("Will use \"%s\" as build id" % |
161 |
_green(self.build_id))
|
162 |
|
163 |
# Set kamaki cloud
|
164 |
if cloud is not None: |
165 |
self.kamaki_cloud = cloud
|
166 |
elif self.config.has_option("Deployment", "kamaki_cloud"): |
167 |
kamaki_cloud = self.config.get("Deployment", "kamaki_cloud") |
168 |
if kamaki_cloud == "": |
169 |
self.kamaki_cloud = None |
170 |
else:
|
171 |
self.kamaki_cloud = None |
172 |
|
173 |
# Initialize variables
|
174 |
self.fabric_installed = False |
175 |
self.kamaki_installed = False |
176 |
self.cyclades_client = None |
177 |
self.network_client = None |
178 |
self.compute_client = None |
179 |
self.image_client = None |
180 |
self.astakos_client = None |
181 |
|
182 |
def setup_kamaki(self): |
183 |
"""Initialize kamaki
|
184 |
|
185 |
Setup cyclades_client, image_client and compute_client
|
186 |
"""
|
187 |
|
188 |
config = kamaki_config.Config() |
189 |
if self.kamaki_cloud is None: |
190 |
try:
|
191 |
self.kamaki_cloud = config.get("global", "default_cloud") |
192 |
except AttributeError: |
193 |
# Compatibility with kamaki version <=0.10
|
194 |
self.kamaki_cloud = config.get("global", "default_cloud") |
195 |
|
196 |
self.logger.info("Setup kamaki client, using cloud '%s'.." % |
197 |
self.kamaki_cloud)
|
198 |
auth_url = config.get_cloud(self.kamaki_cloud, "url") |
199 |
self.logger.debug("Authentication URL is %s" % _green(auth_url)) |
200 |
token = config.get_cloud(self.kamaki_cloud, "token") |
201 |
#self.logger.debug("Token is %s" % _green(token))
|
202 |
|
203 |
self.astakos_client = AstakosClient(auth_url, token)
|
204 |
endpoints = self.astakos_client.authenticate()
|
205 |
|
206 |
cyclades_url = get_endpoint_url(endpoints, "compute")
|
207 |
self.logger.debug("Cyclades API url is %s" % _green(cyclades_url)) |
208 |
self.cyclades_client = CycladesClient(cyclades_url, token)
|
209 |
self.cyclades_client.CONNECTION_RETRY_LIMIT = 2 |
210 |
|
211 |
network_url = get_endpoint_url(endpoints, "network")
|
212 |
self.logger.debug("Network API url is %s" % _green(network_url)) |
213 |
self.network_client = CycladesNetworkClient(network_url, token)
|
214 |
self.network_client.CONNECTION_RETRY_LIMIT = 2 |
215 |
|
216 |
image_url = get_endpoint_url(endpoints, "image")
|
217 |
self.logger.debug("Images API url is %s" % _green(image_url)) |
218 |
self.image_client = ImageClient(cyclades_url, token)
|
219 |
self.image_client.CONNECTION_RETRY_LIMIT = 2 |
220 |
|
221 |
compute_url = get_endpoint_url(endpoints, "compute")
|
222 |
self.logger.debug("Compute API url is %s" % _green(compute_url)) |
223 |
self.compute_client = ComputeClient(compute_url, token)
|
224 |
self.compute_client.CONNECTION_RETRY_LIMIT = 2 |
225 |
|
226 |
def _wait_transition(self, server_id, current_status, new_status): |
227 |
"""Wait for server to go from current_status to new_status"""
|
228 |
self.logger.debug("Waiting for server to become %s" % new_status) |
229 |
timeout = self.config.getint('Global', 'build_timeout') |
230 |
sleep_time = 5
|
231 |
while True: |
232 |
server = self.cyclades_client.get_server_details(server_id)
|
233 |
if server['status'] == new_status: |
234 |
return server
|
235 |
elif timeout < 0: |
236 |
self.logger.error(
|
237 |
"Waiting for server to become %s timed out" % new_status)
|
238 |
self.destroy_server(False) |
239 |
sys.exit(1)
|
240 |
elif server['status'] == current_status: |
241 |
# Sleep for #n secs and continue
|
242 |
timeout = timeout - sleep_time |
243 |
time.sleep(sleep_time) |
244 |
else:
|
245 |
self.logger.error(
|
246 |
"Server failed with status %s" % server['status']) |
247 |
self.destroy_server(False) |
248 |
sys.exit(1)
|
249 |
|
250 |
@_check_kamaki
|
251 |
def destroy_server(self, wait=True): |
252 |
"""Destroy slave server"""
|
253 |
server_id = int(self.read_temp_config('server_id')) |
254 |
fips = [f for f in self.network_client.list_floatingips() |
255 |
if str(f['instance_id']) == str(server_id)] |
256 |
self.logger.info("Destoying server with id %s " % server_id) |
257 |
self.cyclades_client.delete_server(server_id)
|
258 |
if wait:
|
259 |
self._wait_transition(server_id, "ACTIVE", "DELETED") |
260 |
for fip in fips: |
261 |
self.logger.info("Destroying floating ip %s", |
262 |
fip['floating_ip_address'])
|
263 |
self.network_client.delete_floatingip(fip['id']) |
264 |
|
265 |
# pylint: disable= no-self-use
|
266 |
@_check_fabric
|
267 |
def shell_connect(self): |
268 |
"""Open shell to remote server"""
|
269 |
fabric.open_shell("export TERM=xterm")
|
270 |
|
271 |
def _create_floating_ip(self): |
272 |
"""Create a new floating ip"""
|
273 |
networks = self.network_client.list_networks(detail=True) |
274 |
pub_nets = [n for n in networks |
275 |
if n['SNF:floating_ip_pool'] and n['public']] |
276 |
for pub_net in pub_nets: |
277 |
# Try until we find a public network that is not full
|
278 |
try:
|
279 |
fip = self.network_client.create_floatingip(pub_net['id']) |
280 |
except ClientError as err: |
281 |
self.logger.warning("%s: %s", err.message, err.details) |
282 |
continue
|
283 |
self.logger.debug("Floating IP %s with id %s created", |
284 |
fip['floating_ip_address'], fip['id']) |
285 |
return fip
|
286 |
self.logger.error("No mor IP addresses available") |
287 |
sys.exit(1)
|
288 |
|
289 |
def _create_port(self, floating_ip): |
290 |
"""Create a new port for our floating IP"""
|
291 |
net_id = floating_ip['floating_network_id']
|
292 |
self.logger.debug("Creating a new port to network with id %s", net_id) |
293 |
fixed_ips = [{'ip_address': floating_ip['floating_ip_address']}] |
294 |
port = self.network_client.create_port(
|
295 |
net_id, device_id=None, fixed_ips=fixed_ips)
|
296 |
return port
|
297 |
|
298 |
@_check_kamaki
|
299 |
# Too many local variables. pylint: disable-msg=R0914
|
300 |
def create_server(self, image=None, flavor=None, ssh_keys=None, |
301 |
server_name=None):
|
302 |
"""Create slave server"""
|
303 |
self.logger.info("Create a new server..") |
304 |
|
305 |
# Find a build_id to use
|
306 |
self._create_new_build_id()
|
307 |
|
308 |
# Find an image to use
|
309 |
image_id = self._find_image(image)
|
310 |
# Find a flavor to use
|
311 |
flavor_id = self._find_flavor(flavor)
|
312 |
|
313 |
# Create Server
|
314 |
networks = [] |
315 |
if self.config.get("Deployment", "allocate_floating_ip") == "True": |
316 |
fip = self._create_floating_ip()
|
317 |
port = self._create_port(fip)
|
318 |
networks.append({'port': port['id']}) |
319 |
private_networks = self.config.get('Deployment', 'private_networks') |
320 |
if private_networks:
|
321 |
private_networks = [p.strip() for p in private_networks.split(",")] |
322 |
networks.extend([{"uuid": uuid} for uuid in private_networks]) |
323 |
if server_name is None: |
324 |
server_name = self.config.get("Deployment", "server_name") |
325 |
server_name = "%s(BID: %s)" % (server_name, self.build_id) |
326 |
server = self.cyclades_client.create_server(
|
327 |
server_name, flavor_id, image_id, networks=networks) |
328 |
server_id = server['id']
|
329 |
self.write_temp_config('server_id', server_id) |
330 |
self.logger.debug("Server got id %s" % _green(server_id)) |
331 |
server_user = server['metadata']['users'] |
332 |
self.write_temp_config('server_user', server_user) |
333 |
self.logger.debug("Server's admin user is %s" % _green(server_user)) |
334 |
server_passwd = server['adminPass']
|
335 |
self.write_temp_config('server_passwd', server_passwd) |
336 |
|
337 |
server = self._wait_transition(server_id, "BUILD", "ACTIVE") |
338 |
self._get_server_ip_and_port(server, private_networks)
|
339 |
self._copy_ssh_keys(ssh_keys)
|
340 |
|
341 |
# Setup Firewall
|
342 |
self.setup_fabric()
|
343 |
self.logger.info("Setup firewall") |
344 |
accept_ssh_from = self.config.get('Global', 'accept_ssh_from') |
345 |
if accept_ssh_from != "": |
346 |
self.logger.debug("Block ssh except from %s" % accept_ssh_from) |
347 |
cmd = """
|
348 |
local_ip=$(/sbin/ifconfig eth0 | grep 'inet addr:' | \
|
349 |
cut -d':' -f2 | cut -d' ' -f1)
|
350 |
iptables -A INPUT -s localhost -j ACCEPT
|
351 |
iptables -A INPUT -s $local_ip -j ACCEPT
|
352 |
iptables -A INPUT -s {0} -p tcp --dport 22 -j ACCEPT
|
353 |
iptables -A INPUT -p tcp --dport 22 -j DROP
|
354 |
""".format(accept_ssh_from)
|
355 |
_run(cmd, False)
|
356 |
|
357 |
# Setup apt, download packages
|
358 |
self.logger.debug("Setup apt. Install x2goserver and firefox") |
359 |
cmd = """
|
360 |
echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
|
361 |
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
362 |
apt-get update
|
363 |
apt-get install curl --yes --force-yes
|
364 |
echo -e "\n\n{0}" >> /etc/apt/sources.list
|
365 |
# Synnefo repo's key
|
366 |
curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -
|
367 |
|
368 |
# X2GO Key
|
369 |
apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E
|
370 |
apt-get install x2go-keyring --yes --force-yes
|
371 |
apt-get update
|
372 |
apt-get install x2goserver x2goserver-xsession \
|
373 |
iceweasel --yes --force-yes
|
374 |
|
375 |
# xterm published application
|
376 |
echo '[Desktop Entry]' > /usr/share/applications/xterm.desktop
|
377 |
echo 'Name=XTerm' >> /usr/share/applications/xterm.desktop
|
378 |
echo 'Comment=standard terminal emulator for the X window system' >> \
|
379 |
/usr/share/applications/xterm.desktop
|
380 |
echo 'Exec=xterm' >> /usr/share/applications/xterm.desktop
|
381 |
echo 'Terminal=false' >> /usr/share/applications/xterm.desktop
|
382 |
echo 'Type=Application' >> /usr/share/applications/xterm.desktop
|
383 |
echo 'Encoding=UTF-8' >> /usr/share/applications/xterm.desktop
|
384 |
echo 'Icon=xterm-color_48x48' >> /usr/share/applications/xterm.desktop
|
385 |
echo 'Categories=System;TerminalEmulator;' >> \
|
386 |
/usr/share/applications/xterm.desktop
|
387 |
""".format(self.config.get('Global', 'apt_repo')) |
388 |
_run(cmd, False)
|
389 |
|
390 |
def _find_flavor(self, flavor=None): |
391 |
"""Find a suitable flavor to use
|
392 |
|
393 |
Search by name (reg expression) or by id
|
394 |
"""
|
395 |
# Get a list of flavors from config file
|
396 |
flavors = self.config.get('Deployment', 'flavors').split(",") |
397 |
if flavor is not None: |
398 |
# If we have a flavor_name to use, add it to our list
|
399 |
flavors.insert(0, flavor)
|
400 |
|
401 |
list_flavors = self.compute_client.list_flavors()
|
402 |
for flv in flavors: |
403 |
flv_type, flv_value = parse_typed_option(option="flavor",
|
404 |
value=flv) |
405 |
if flv_type == "name": |
406 |
# Filter flavors by name
|
407 |
self.logger.debug(
|
408 |
"Trying to find a flavor with name \"%s\"" % flv_value)
|
409 |
list_flvs = \ |
410 |
[f for f in list_flavors |
411 |
if re.search(flv_value, f['name'], flags=re.I) |
412 |
is not None] |
413 |
elif flv_type == "id": |
414 |
# Filter flavors by id
|
415 |
self.logger.debug(
|
416 |
"Trying to find a flavor with id \"%s\"" % flv_value)
|
417 |
list_flvs = \ |
418 |
[f for f in list_flavors |
419 |
if str(f['id']) == flv_value] |
420 |
else:
|
421 |
self.logger.error("Unrecognized flavor type %s" % flv_type) |
422 |
|
423 |
# Check if we found one
|
424 |
if list_flvs:
|
425 |
self.logger.debug("Will use \"%s\" with id \"%s\"" |
426 |
% (_green(list_flvs[0]['name']), |
427 |
_green(list_flvs[0]['id']))) |
428 |
return list_flvs[0]['id'] |
429 |
|
430 |
self.logger.error("No matching flavor found.. aborting") |
431 |
sys.exit(1)
|
432 |
|
433 |
def _find_image(self, image=None): |
434 |
"""Find a suitable image to use
|
435 |
|
436 |
In case of search by name, the image has to belong to one
|
437 |
of the `DEFAULT_SYSTEM_IMAGES_UUID' users.
|
438 |
In case of search by id it only has to exist.
|
439 |
"""
|
440 |
# Get a list of images from config file
|
441 |
images = self.config.get('Deployment', 'images').split(",") |
442 |
if image is not None: |
443 |
# If we have an image from command line, add it to our list
|
444 |
images.insert(0, image)
|
445 |
|
446 |
auth = self.astakos_client.authenticate()
|
447 |
user_uuid = auth["access"]["token"]["tenant"]["id"] |
448 |
list_images = self.image_client.list_public(detail=True)['images'] |
449 |
for img in images: |
450 |
img_type, img_value = parse_typed_option(option="image", value=img)
|
451 |
if img_type == "name": |
452 |
# Filter images by name
|
453 |
self.logger.debug(
|
454 |
"Trying to find an image with name \"%s\"" % img_value)
|
455 |
accepted_uuids = DEFAULT_SYSTEM_IMAGES_UUID + [user_uuid] |
456 |
list_imgs = \ |
457 |
[i for i in list_images if i['user_id'] in accepted_uuids |
458 |
and
|
459 |
re.search(img_value, i['name'], flags=re.I) is not None] |
460 |
elif img_type == "id": |
461 |
# Filter images by id
|
462 |
self.logger.debug(
|
463 |
"Trying to find an image with id \"%s\"" % img_value)
|
464 |
list_imgs = \ |
465 |
[i for i in list_images |
466 |
if i['id'].lower() == img_value.lower()] |
467 |
else:
|
468 |
self.logger.error("Unrecognized image type %s" % img_type) |
469 |
sys.exit(1)
|
470 |
|
471 |
# Check if we found one
|
472 |
if list_imgs:
|
473 |
self.logger.debug("Will use \"%s\" with id \"%s\"" |
474 |
% (_green(list_imgs[0]['name']), |
475 |
_green(list_imgs[0]['id']))) |
476 |
return list_imgs[0]['id'] |
477 |
|
478 |
# We didn't found one
|
479 |
self.logger.error("No matching image found.. aborting") |
480 |
sys.exit(1)
|
481 |
|
482 |
def _get_server_ip_and_port(self, server, private_networks): |
483 |
"""Compute server's IPv4 and ssh port number"""
|
484 |
self.logger.info("Get server connection details..") |
485 |
if private_networks:
|
486 |
# Choose the networks that belong to private_networks
|
487 |
networks = [n for n in server['attachments'] |
488 |
if n['network_id'] in private_networks] |
489 |
else:
|
490 |
# Choose the networks that are public
|
491 |
networks = [n for n in server['attachments'] |
492 |
if self.network_client. |
493 |
get_network_details(n['network_id'])['public']] |
494 |
# Choose the networks with IPv4
|
495 |
networks = [n for n in networks if n['ipv4']] |
496 |
# Use the first network as IPv4
|
497 |
server_ip = networks[0]['ipv4'] |
498 |
|
499 |
if (".okeanos.io" in self.cyclades_client.base_url or |
500 |
".demo.synnefo.org" in self.cyclades_client.base_url): |
501 |
tmp1 = int(server_ip.split(".")[2]) |
502 |
tmp2 = int(server_ip.split(".")[3]) |
503 |
server_ip = "gate.okeanos.io"
|
504 |
server_port = 10000 + tmp1 * 256 + tmp2 |
505 |
else:
|
506 |
server_port = 22
|
507 |
self.write_temp_config('server_ip', server_ip) |
508 |
self.logger.debug("Server's IPv4 is %s" % _green(server_ip)) |
509 |
self.write_temp_config('server_port', server_port) |
510 |
self.logger.debug("Server's ssh port is %s" % _green(server_port)) |
511 |
ssh_command = "ssh -p %s %s@%s" \
|
512 |
% (server_port, server['metadata']['users'], server_ip) |
513 |
self.logger.debug("Access server using \"%s\"" % |
514 |
(_green(ssh_command))) |
515 |
|
516 |
@_check_fabric
|
517 |
def _copy_ssh_keys(self, ssh_keys): |
518 |
"""Upload/Install ssh keys to server"""
|
519 |
self.logger.debug("Check for authentication keys to use") |
520 |
if ssh_keys is None: |
521 |
ssh_keys = self.config.get("Deployment", "ssh_keys") |
522 |
|
523 |
if ssh_keys != "": |
524 |
ssh_keys = os.path.expanduser(ssh_keys) |
525 |
self.logger.debug("Will use \"%s\" authentication keys file" % |
526 |
_green(ssh_keys)) |
527 |
keyfile = '/tmp/%s.pub' % fabric.env.user
|
528 |
_run('mkdir -p ~/.ssh && chmod 700 ~/.ssh', False) |
529 |
if ssh_keys.startswith("http://") or \ |
530 |
ssh_keys.startswith("https://") or \ |
531 |
ssh_keys.startswith("ftp://"):
|
532 |
cmd = """
|
533 |
apt-get update
|
534 |
apt-get install wget --yes --force-yes
|
535 |
wget {0} -O {1} --no-check-certificate
|
536 |
""".format(ssh_keys, keyfile)
|
537 |
_run(cmd, False)
|
538 |
elif os.path.exists(ssh_keys):
|
539 |
_put(ssh_keys, keyfile) |
540 |
else:
|
541 |
self.logger.debug("No ssh keys found") |
542 |
return
|
543 |
_run('cat %s >> ~/.ssh/authorized_keys' % keyfile, False) |
544 |
_run('rm %s' % keyfile, False) |
545 |
self.logger.debug("Uploaded ssh authorized keys") |
546 |
else:
|
547 |
self.logger.debug("No ssh keys found") |
548 |
|
549 |
def _create_new_build_id(self): |
550 |
"""Find a uniq build_id to use"""
|
551 |
with filelocker.lock("%s.lock" % self.temp_config_file, |
552 |
filelocker.LOCK_EX): |
553 |
# Read temp_config again to get any new entries
|
554 |
self.temp_config.read(self.temp_config_file) |
555 |
|
556 |
# Find a uniq build_id to use
|
557 |
if self.build_id is None: |
558 |
ids = self.temp_config.sections()
|
559 |
if ids:
|
560 |
max_id = int(max(self.temp_config.sections(), key=int)) |
561 |
self.build_id = max_id + 1 |
562 |
else:
|
563 |
self.build_id = 1 |
564 |
self.logger.debug("Will use \"%s\" as build id" |
565 |
% _green(self.build_id))
|
566 |
|
567 |
# Create a new section
|
568 |
try:
|
569 |
self.temp_config.add_section(str(self.build_id)) |
570 |
except DuplicateSectionError:
|
571 |
msg = ("Build id \"%s\" already in use. " +
|
572 |
"Please use a uniq one or cleanup \"%s\" file.\n") \
|
573 |
% (self.build_id, self.temp_config_file) |
574 |
self.logger.error(msg)
|
575 |
sys.exit(1)
|
576 |
creation_time = \ |
577 |
time.strftime("%a, %d %b %Y %X", time.localtime())
|
578 |
self.temp_config.set(str(self.build_id), |
579 |
"created", str(creation_time)) |
580 |
|
581 |
# Write changes back to temp config file
|
582 |
with open(self.temp_config_file, 'wb') as tcf: |
583 |
self.temp_config.write(tcf)
|
584 |
|
585 |
def write_temp_config(self, option, value): |
586 |
"""Write changes back to config file"""
|
587 |
# Acquire the lock to write to temp_config_file
|
588 |
with filelocker.lock("%s.lock" % self.temp_config_file, |
589 |
filelocker.LOCK_EX): |
590 |
|
591 |
# Read temp_config again to get any new entries
|
592 |
self.temp_config.read(self.temp_config_file) |
593 |
|
594 |
self.temp_config.set(str(self.build_id), option, str(value)) |
595 |
curr_time = time.strftime("%a, %d %b %Y %X", time.localtime())
|
596 |
self.temp_config.set(str(self.build_id), "modified", curr_time) |
597 |
|
598 |
# Write changes back to temp config file
|
599 |
with open(self.temp_config_file, 'wb') as tcf: |
600 |
self.temp_config.write(tcf)
|
601 |
|
602 |
def read_temp_config(self, option): |
603 |
"""Read from temporary_config file"""
|
604 |
# If build_id is None use the latest one
|
605 |
if self.build_id is None: |
606 |
ids = self.temp_config.sections()
|
607 |
if ids:
|
608 |
self.build_id = int(ids[-1]) |
609 |
else:
|
610 |
self.logger.error("No sections in temporary config file") |
611 |
sys.exit(1)
|
612 |
self.logger.debug("Will use \"%s\" as build id" |
613 |
% _green(self.build_id))
|
614 |
# Read specified option
|
615 |
return self.temp_config.get(str(self.build_id), option) |
616 |
|
617 |
def setup_fabric(self): |
618 |
"""Setup fabric environment"""
|
619 |
self.logger.info("Setup fabric parameters..") |
620 |
fabric.env.user = self.read_temp_config('server_user') |
621 |
fabric.env.host_string = self.read_temp_config('server_ip') |
622 |
fabric.env.port = int(self.read_temp_config('server_port')) |
623 |
fabric.env.password = self.read_temp_config('server_passwd') |
624 |
fabric.env.connection_attempts = 10
|
625 |
fabric.env.shell = "/bin/bash -c"
|
626 |
fabric.env.disable_known_hosts = True
|
627 |
fabric.env.output_prefix = None
|
628 |
|
629 |
def _check_hash_sum(self, localfile, remotefile): |
630 |
"""Check hash sums of two files"""
|
631 |
self.logger.debug("Check hash sum for local file %s" % localfile) |
632 |
hash1 = os.popen("sha256sum %s" % localfile).read().split(' ')[0] |
633 |
self.logger.debug("Local file has sha256 hash %s" % hash1) |
634 |
self.logger.debug("Check hash sum for remote file %s" % remotefile) |
635 |
hash2 = _run("sha256sum %s" % remotefile, False) |
636 |
hash2 = hash2.split(' ')[0] |
637 |
self.logger.debug("Remote file has sha256 hash %s" % hash2) |
638 |
if hash1 != hash2:
|
639 |
self.logger.error("Hashes differ.. aborting") |
640 |
sys.exit(1)
|
641 |
|
642 |
@_check_fabric
|
643 |
def clone_repo(self, local_repo=False): |
644 |
"""Clone Synnefo repo from slave server"""
|
645 |
self.logger.info("Configure repositories on remote server..") |
646 |
self.logger.debug("Install/Setup git") |
647 |
cmd = """
|
648 |
apt-get install git --yes --force-yes
|
649 |
git config --global user.name {0}
|
650 |
git config --global user.email {1}
|
651 |
""".format(self.config.get('Global', 'git_config_name'), |
652 |
self.config.get('Global', 'git_config_mail')) |
653 |
_run(cmd, False)
|
654 |
|
655 |
# Clone synnefo_repo
|
656 |
synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo)
|
657 |
# Clone pithos-web-client
|
658 |
self.clone_pithos_webclient_repo(synnefo_branch)
|
659 |
|
660 |
@_check_fabric
|
661 |
def clone_synnefo_repo(self, local_repo=False): |
662 |
"""Clone Synnefo repo to remote server"""
|
663 |
# Find synnefo_repo and synnefo_branch to use
|
664 |
synnefo_repo = self.config.get('Global', 'synnefo_repo') |
665 |
synnefo_branch = self.config.get("Global", "synnefo_branch") |
666 |
if synnefo_branch == "": |
667 |
synnefo_branch = \ |
668 |
subprocess.Popen( |
669 |
["git", "rev-parse", "--abbrev-ref", "HEAD"], |
670 |
stdout=subprocess.PIPE).communicate()[0].strip()
|
671 |
if synnefo_branch == "HEAD": |
672 |
synnefo_branch = \ |
673 |
subprocess.Popen( |
674 |
["git", "rev-parse", "--short", "HEAD"], |
675 |
stdout=subprocess.PIPE).communicate()[0].strip()
|
676 |
self.logger.debug("Will use branch \"%s\"" % _green(synnefo_branch)) |
677 |
|
678 |
if local_repo or synnefo_repo == "": |
679 |
# Use local_repo
|
680 |
self.logger.debug("Push local repo to server") |
681 |
# Firstly create the remote repo
|
682 |
_run("git init synnefo", False) |
683 |
# Then push our local repo over ssh
|
684 |
# We have to pass some arguments to ssh command
|
685 |
# namely to disable host checking.
|
686 |
(temp_ssh_file_handle, temp_ssh_file) = tempfile.mkstemp() |
687 |
os.close(temp_ssh_file_handle) |
688 |
# XXX: git push doesn't read the password
|
689 |
cmd = """
|
690 |
echo 'exec ssh -o "StrictHostKeyChecking no" \
|
691 |
-o "UserKnownHostsFile /dev/null" \
|
692 |
-q "$@"' > {4}
|
693 |
chmod u+x {4}
|
694 |
export GIT_SSH="{4}"
|
695 |
echo "{0}" | git push --quiet --mirror ssh://{1}@{2}:{3}/~/synnefo
|
696 |
rm -f {4}
|
697 |
""".format(fabric.env.password,
|
698 |
fabric.env.user, |
699 |
fabric.env.host_string, |
700 |
fabric.env.port, |
701 |
temp_ssh_file) |
702 |
os.system(cmd) |
703 |
else:
|
704 |
# Clone Synnefo from remote repo
|
705 |
self.logger.debug("Clone synnefo from %s" % synnefo_repo) |
706 |
self._git_clone(synnefo_repo)
|
707 |
|
708 |
# Checkout the desired synnefo_branch
|
709 |
self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch) |
710 |
cmd = """
|
711 |
cd synnefo
|
712 |
for branch in `git branch -a | grep remotes | grep -v HEAD`; do
|
713 |
git branch --track ${branch##*/} $branch
|
714 |
done
|
715 |
git checkout %s
|
716 |
""" % (synnefo_branch)
|
717 |
_run(cmd, False)
|
718 |
|
719 |
return synnefo_branch
|
720 |
|
721 |
@_check_fabric
|
722 |
def clone_pithos_webclient_repo(self, synnefo_branch): |
723 |
"""Clone Pithos WebClient repo to remote server"""
|
724 |
# Find pithos_webclient_repo and pithos_webclient_branch to use
|
725 |
pithos_webclient_repo = \ |
726 |
self.config.get('Global', 'pithos_webclient_repo') |
727 |
pithos_webclient_branch = \ |
728 |
self.config.get('Global', 'pithos_webclient_branch') |
729 |
|
730 |
# Clone pithos-webclient from remote repo
|
731 |
self.logger.debug("Clone pithos-webclient from %s" % |
732 |
pithos_webclient_repo) |
733 |
self._git_clone(pithos_webclient_repo)
|
734 |
|
735 |
# Track all pithos-webclient branches
|
736 |
cmd = """
|
737 |
cd pithos-web-client
|
738 |
for branch in `git branch -a | grep remotes | grep -v HEAD`; do
|
739 |
git branch --track ${branch##*/} $branch > /dev/null 2>&1
|
740 |
done
|
741 |
git --no-pager branch --no-color
|
742 |
"""
|
743 |
webclient_branches = _run(cmd, False)
|
744 |
webclient_branches = webclient_branches.split() |
745 |
|
746 |
# If we have pithos_webclient_branch in config file use this one
|
747 |
# else try to use the same branch as synnefo_branch
|
748 |
# else use an appropriate one.
|
749 |
if pithos_webclient_branch == "": |
750 |
if synnefo_branch in webclient_branches: |
751 |
pithos_webclient_branch = synnefo_branch |
752 |
else:
|
753 |
# If synnefo_branch starts with one of
|
754 |
# 'master', 'hotfix'; use the master branch
|
755 |
if synnefo_branch.startswith('master') or \ |
756 |
synnefo_branch.startswith('hotfix'):
|
757 |
pithos_webclient_branch = "master"
|
758 |
# If synnefo_branch starts with one of
|
759 |
# 'develop', 'feature'; use the develop branch
|
760 |
elif synnefo_branch.startswith('develop') or \ |
761 |
synnefo_branch.startswith('feature'):
|
762 |
pithos_webclient_branch = "develop"
|
763 |
else:
|
764 |
self.logger.warning(
|
765 |
"Cannot determine which pithos-web-client branch to "
|
766 |
"use based on \"%s\" synnefo branch. "
|
767 |
"Will use develop." % synnefo_branch)
|
768 |
pithos_webclient_branch = "develop"
|
769 |
# Checkout branch
|
770 |
self.logger.debug("Checkout \"%s\" branch" % |
771 |
_green(pithos_webclient_branch)) |
772 |
cmd = """
|
773 |
cd pithos-web-client
|
774 |
git checkout {0}
|
775 |
""".format(pithos_webclient_branch)
|
776 |
_run(cmd, False)
|
777 |
|
778 |
def _git_clone(self, repo): |
779 |
"""Clone repo to remote server
|
780 |
|
781 |
Currently clonning from code.grnet.gr can fail unexpectedly.
|
782 |
So retry!!
|
783 |
|
784 |
"""
|
785 |
cloned = False
|
786 |
for i in range(1, 11): |
787 |
try:
|
788 |
_run("git clone %s" % repo, False) |
789 |
cloned = True
|
790 |
break
|
791 |
except BaseException: |
792 |
self.logger.warning("Clonning failed.. retrying %s/10" % i) |
793 |
if not cloned: |
794 |
self.logger.error("Can not clone repo.") |
795 |
sys.exit(1)
|
796 |
|
797 |
@_check_fabric
|
798 |
def build_packages(self): |
799 |
"""Build packages needed by Synnefo software"""
|
800 |
self.logger.info("Install development packages") |
801 |
cmd = """
|
802 |
apt-get update
|
803 |
apt-get install zlib1g-dev dpkg-dev debhelper git-buildpackage \
|
804 |
python-dev python-all python-pip ant --yes --force-yes
|
805 |
pip install -U devflow
|
806 |
"""
|
807 |
_run(cmd, False)
|
808 |
|
809 |
# Patch pydist bug
|
810 |
if self.config.get('Global', 'patch_pydist') == "True": |
811 |
self.logger.debug("Patch pydist.py module") |
812 |
cmd = r"""
|
813 |
sed -r -i 's/(\(\?P<name>\[A-Za-z\]\[A-Za-z0-9_\.)/\1\\\-/' \ |
814 |
/usr/share/python/debpython/pydist.py |
815 |
"""
|
816 |
_run(cmd, False)
|
817 |
|
818 |
# Build synnefo packages
|
819 |
self.build_synnefo()
|
820 |
# Build pithos-web-client packages
|
821 |
self.build_pithos_webclient()
|
822 |
|
823 |
@_check_fabric
|
824 |
def build_synnefo(self):
|
825 |
"""Build Synnefo packages""" |
826 |
self.logger.info("Build Synnefo packages..")
|
827 |
|
828 |
cmd = """
|
829 |
devflow-autopkg snapshot -b ~/synnefo_build-area --no-sign |
830 |
"""
|
831 |
with fabric.cd("synnefo"):
|
832 |
_run(cmd, True)
|
833 |
|
834 |
# Install snf-deploy package
|
835 |
self.logger.debug("Install snf-deploy package")
|
836 |
cmd = """
|
837 |
dpkg -i snf-deploy*.deb |
838 |
apt-get -f install --yes --force-yes |
839 |
"""
|
840 |
with fabric.cd("synnefo_build-area"):
|
841 |
with fabric.settings(warn_only=True):
|
842 |
_run(cmd, True)
|
843 |
|
844 |
# Setup synnefo packages for snf-deploy
|
845 |
self.logger.debug("Copy synnefo debs to snf-deploy packages dir")
|
846 |
cmd = """
|
847 |
cp ~/synnefo_build-area/*.deb /var/lib/snf-deploy/packages/ |
848 |
"""
|
849 |
_run(cmd, False)
|
850 |
|
851 |
@_check_fabric
|
852 |
def build_pithos_webclient(self):
|
853 |
"""Build pithos-web-client packages""" |
854 |
self.logger.info("Build pithos-web-client packages..")
|
855 |
|
856 |
cmd = """
|
857 |
devflow-autopkg snapshot -b ~/webclient_build-area --no-sign |
858 |
"""
|
859 |
with fabric.cd("pithos-web-client"):
|
860 |
_run(cmd, True)
|
861 |
|
862 |
# Setup pithos-web-client packages for snf-deploy
|
863 |
self.logger.debug("Copy webclient debs to snf-deploy packages dir")
|
864 |
cmd = """
|
865 |
cp ~/webclient_build-area/*.deb /var/lib/snf-deploy/packages/ |
866 |
"""
|
867 |
_run(cmd, False)
|
868 |
|
869 |
@_check_fabric
|
870 |
def build_documentation(self):
|
871 |
"""Build Synnefo documentation""" |
872 |
self.logger.info("Build Synnefo documentation..")
|
873 |
_run("pip install -U Sphinx", False)
|
874 |
with fabric.cd("synnefo"):
|
875 |
_run("devflow-update-version; "
|
876 |
"./ci/make_docs.sh synnefo_documentation", False)
|
877 |
|
878 |
def fetch_documentation(self, dest=None):
|
879 |
"""Fetch Synnefo documentation""" |
880 |
self.logger.info("Fetch Synnefo documentation..")
|
881 |
if dest is None:
|
882 |
dest = "synnefo_documentation"
|
883 |
dest = os.path.abspath(dest)
|
884 |
if not os.path.exists(dest):
|
885 |
os.makedirs(dest)
|
886 |
self.fetch_compressed("synnefo/synnefo_documentation", dest)
|
887 |
self.logger.info("Downloaded documentation to %s" %
|
888 |
_green(dest))
|
889 |
|
890 |
@_check_fabric
|
891 |
def deploy_synnefo(self, schema=None):
|
892 |
"""Deploy Synnefo using snf-deploy""" |
893 |
self.logger.info("Deploy Synnefo..")
|
894 |
if schema is None:
|
895 |
schema = self.config.get('Global', 'schema')
|
896 |
self.logger.debug("Will use \"%s\" schema" % _green(schema))
|
897 |
|
898 |
schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema)
|
899 |
if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)):
|
900 |
raise ValueError("Unknown schema: %s" % schema)
|
901 |
|
902 |
self.logger.debug("Upload schema files to server")
|
903 |
_put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/")
|
904 |
|
905 |
self.logger.debug("Change password in nodes.conf file")
|
906 |
cmd = """
|
907 |
sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
|
908 |
sed -i 's/12345/{0}/' /etc/snf-deploy/nodes.conf
|
909 |
""".format(fabric.env.password)
|
910 |
_run(cmd, False)
|
911 |
|
912 |
self.logger.debug("Run snf-deploy")
|
913 |
cmd = """
|
914 |
snf-deploy keygen --force |
915 |
snf-deploy --disable-colors --autoconf all
|
916 |
"""
|
917 |
_run(cmd, True)
|
918 |
|
919 |
@_check_fabric
|
920 |
def unit_test(self):
|
921 |
"""Run Synnefo unit test suite""" |
922 |
self.logger.info("Run Synnefo unit test suite")
|
923 |
component = self.config.get('Unit Tests', 'component')
|
924 |
|
925 |
self.logger.debug("Install needed packages")
|
926 |
cmd = """
|
927 |
pip install -U mock |
928 |
pip install -U factory_boy |
929 |
pip install -U nose |
930 |
"""
|
931 |
_run(cmd, False)
|
932 |
|
933 |
self.logger.debug("Upload tests.sh file")
|
934 |
unit_tests_file = os.path.join(self.ci_dir, "tests.sh")
|
935 |
_put(unit_tests_file, ".")
|
936 |
|
937 |
self.logger.debug("Run unit tests")
|
938 |
cmd = """
|
939 |
bash tests.sh {0}
|
940 |
""".format(component)
|
941 |
_run(cmd, True)
|
942 |
|
943 |
@_check_fabric
|
944 |
def run_burnin(self):
|
945 |
"""Run burnin functional test suite""" |
946 |
self.logger.info("Run Burnin functional test suite")
|
947 |
cmd = """
|
948 |
auth_url=$(grep -e '^url =' .kamakirc | cut -d' ' -f3) |
949 |
token=$(grep -e '^token =' .kamakirc | cut -d' ' -f3) |
950 |
images_user=$(kamaki image list -l | grep owner | \ |
951 |
cut -d':' -f2 | tr -d ' ') |
952 |
snf-burnin --auth-url=$auth_url --token=$token {0} |
953 |
BurninExitStatus=$? |
954 |
exit $BurninExitStatus |
955 |
""".format(self.config.get('Burnin', 'cmd_options'))
|
956 |
_run(cmd, True)
|
957 |
|
958 |
@_check_fabric
|
959 |
def fetch_compressed(self, src, dest=None):
|
960 |
"""Create a tarball and fetch it locally""" |
961 |
self.logger.debug("Creating tarball of %s" % src)
|
962 |
basename = os.path.basename(src)
|
963 |
tar_file = basename + ".tgz"
|
964 |
cmd = "tar czf %s %s" % (tar_file, src)
|
965 |
_run(cmd, False)
|
966 |
if not os.path.exists(dest):
|
967 |
os.makedirs(dest)
|
968 |
|
969 |
tmp_dir = tempfile.mkdtemp()
|
970 |
fabric.get(tar_file, tmp_dir)
|
971 |
|
972 |
dest_file = os.path.join(tmp_dir, tar_file)
|
973 |
self._check_hash_sum(dest_file, tar_file)
|
974 |
self.logger.debug("Untar packages file %s" % dest_file)
|
975 |
cmd = """
|
976 |
cd %s |
977 |
tar xzf %s |
978 |
cp -r %s/* %s |
979 |
rm -r %s |
980 |
""" % (tmp_dir, tar_file, src, dest, tmp_dir)
|
981 |
os.system(cmd)
|
982 |
self.logger.info("Downloaded %s to %s" %
|
983 |
(src, _green(dest)))
|
984 |
|
985 |
@_check_fabric
|
986 |
def fetch_packages(self, dest=None):
|
987 |
"""Fetch Synnefo packages""" |
988 |
if dest is None:
|
989 |
dest = self.config.get('Global', 'pkgs_dir')
|
990 |
dest = os.path.abspath(os.path.expanduser(dest))
|
991 |
if not os.path.exists(dest):
|
992 |
os.makedirs(dest)
|
993 |
self.fetch_compressed("synnefo_build-area", dest)
|
994 |
self.fetch_compressed("webclient_build-area", dest)
|
995 |
self.logger.info("Downloaded debian packages to %s" %
|
996 |
_green(dest))
|
997 |
|
998 |
def x2go_plugin(self, dest=None):
|
999 |
"""Produce an html page which will use the x2goplugin
|
1000 |
|
1001 |
Arguments: |
1002 |
dest -- The file where to save the page (String)
|
1003 |
|
1004 |
"""
|
1005 |
output_str = """
|
1006 |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
1007 |
<html> |
1008 |
<head> |
1009 |
<title>X2Go SynnefoCI Service</title> |
1010 |
</head> |
1011 |
<body onload="checkPlugin()">
|
1012 |
<div id="x2goplugin">
|
1013 |
<object
|
1014 |
src="location"
|
1015 |
type="application/x2go"
|
1016 |
name="x2goplugin"
|
1017 |
palette="background"
|
1018 |
height="100%"
|
1019 |
hspace="0"
|
1020 |
vspace="0"
|
1021 |
width="100%"
|
1022 |
x2goconfig="
|
1023 |
session=X2Go-SynnefoCI-Session |
1024 |
server={0}
|
1025 |
user={1}
|
1026 |
sshport={2}
|
1027 |
published=true |
1028 |
autologin=true |
1029 |
">
|
1030 |
</object>
|
1031 |
</div> |
1032 |
</body> |
1033 |
</html> |
1034 |
""".format(self.read_temp_config('server_ip'),
|
1035 |
self.read_temp_config('server_user'),
|
1036 |
self.read_temp_config('server_port'))
|
1037 |
if dest is None:
|
1038 |
dest = self.config.get('Global', 'x2go_plugin_file')
|
1039 |
|
1040 |
self.logger.info("Writting x2go plugin html file to %s" % dest)
|
1041 |
fid = open(dest, 'w')
|
1042 |
fid.write(output_str)
|
1043 |
fid.close()
|
1044 |
|
1045 |
|
1046 |
def parse_typed_option(option, value):
|
1047 |
"""Parsed typed options (flavors and images)""" |
1048 |
try:
|
1049 |
[type_, val] = value.strip().split(':')
|
1050 |
if type_ not in ["id", "name"]:
|
1051 |
raise ValueError
|
1052 |
return type_, val
|
1053 |
except ValueError:
|
1054 |
msg = "Invalid %s format. Must be [id|name]:.+" % option
|
1055 |
raise ValueError(msg)
|
1056 |
|
1057 |
|
1058 |
def get_endpoint_url(endpoints, endpoint_type):
|
1059 |
"""Get the publicURL for the specified endpoint""" |
1060 |
|
1061 |
service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
|
1062 |
return service_catalog[0]['endpoints'][0]['publicURL']
|
1063 |
|