anvil/devstack/utils.py

682 lines
19 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
#
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import os
import random
import re
import socket
import sys
import tempfile
import distutils.version
import netifaces
import progressbar
import termcolor
from devstack import colorlog
from devstack import date
from devstack import exceptions as excp
from devstack import log as logging
from devstack import settings
from devstack import shell as sh
from devstack import version
# The pattern will match either a comment to the EOL, or a
# token to be subbed. The replacer will check which it got and
# act accordingly. Note that we need the MULTILINE flag
# for the comment checks to work in a string containing newlines
PARAM_SUB_REGEX = re.compile(r"#.*$|%([\w\d]+?)%", re.MULTILINE)
EXT_COMPONENT = re.compile(r"^\s*([\w-]+)(?:\((.*)\))?\s*$")
MONTY_PYTHON_TEXT_RE = re.compile("([a-z0-9A-Z\?!.,'\"]+)")
LOG = logging.getLogger("devstack.util")
DEF_IP = "127.0.0.1"
IP_LOOKER = '8.8.8.8'
DEF_IP_VERSION = settings.IPV4
ALL_NUMS = re.compile(r"^\d+$")
START_NUMS = re.compile(r"^(\d+)(\D+)")
STAR_VERSION = 0
# Thx cowsay
# See: http://www.nog.net/~tony/warez/cowsay.shtml
COWS = dict()
COWS['happy'] = r'''
{header}
\ {ear}__{ear}
\ ({eye}{eye})\_______
(__)\ )\/\
||----w |
|| ||
'''
COWS['unhappy'] = r'''
{header}
\ || ||
\ __ ||-----mm||
\ ( )/_________)//
({eye}{eye})/
{ear}--{ear}
'''
def construct_log_level(verbosity_level, dry_run=False):
log_level = logging.INFO
if verbosity_level >= 3:
log_level = logging.DEBUG
elif verbosity_level == 2 or dry_run:
log_level = logging.AUDIT
return log_level
def configure_logging(log_level, cli_args):
root_logger = logging.getLogger().logger
console_logger = logging.StreamHandler(sys.stdout)
console_format = '%(levelname)s: @%(name)s : %(message)s'
if sh.in_terminal():
console_logger.setFormatter(colorlog.TermFormatter(console_format))
else:
console_logger.setFormatter(logging.Formatter(console_format))
root_logger.addHandler(console_logger)
root_logger.setLevel(log_level)
def load_template(component, template_name):
templ_pth = sh.joinpths(settings.STACK_TEMPLATE_DIR, component, template_name)
return (templ_pth, sh.load_file(templ_pth))
def execute_template(*cmds, **kargs):
params_replacements = kargs.pop('params', None)
ignore_missing = kargs.pop('ignore_missing', False)
cmd_results = list()
for cmdinfo in cmds:
cmd_to_run_templ = cmdinfo["cmd"]
cmd_to_run = param_replace_list(cmd_to_run_templ, params_replacements, ignore_missing)
stdin_templ = cmdinfo.get('stdin')
stdin = None
if stdin_templ:
stdin_full = param_replace_list(stdin_templ, params_replacements, ignore_missing)
stdin = joinlinesep(*stdin_full)
exec_result = sh.execute(*cmd_to_run,
run_as_root=cmdinfo.get('run_as_root', False),
process_input=stdin,
ignore_exit_code=cmdinfo.get('ignore_failure', False),
**kargs)
cmd_results.append(exec_result)
return cmd_results
def to_bytes(text):
byte_val = 0
if not text:
return byte_val
if text[-1].upper() == 'G':
byte_val = int(text[:-1]) * 1024 ** 3
elif text[-1].upper() == 'M':
byte_val = int(text[:-1]) * 1024 ** 2
elif text[-1].upper() == 'K':
byte_val = int(text[:-1]) * 1024
elif text[-1].upper() == 'B':
byte_val = int(text[:-1])
else:
byte_val = int(text)
return byte_val
def mark_unexecute_file(fn, kvs, comment_start='#'):
add_lines = list()
add_lines.append('')
add_lines.append(comment_start + ' Ran on %s by %s' % (date.rcf8222date(), sh.getuser()))
add_lines.append(comment_start + ' With data:')
for (k, v) in kvs.items():
add_lines.append(comment_start + ' %s => %s' % (k, v))
sh.append_file(fn, joinlinesep(*add_lines))
sh.chmod(fn, 0644)
def log_iterable(to_log, header=None, logger=None):
if not logger:
logger = LOG
if header:
logger.info(header)
for c in to_log:
logger.info("|-- %s", color_text(c, 'blue'))
@contextlib.contextmanager
def progress_bar(name, max_am, reverse=False):
widgets = list()
widgets.append('%s: ' % (name))
widgets.append(progressbar.Percentage())
widgets.append(' ')
if reverse:
widgets.append(progressbar.ReverseBar())
else:
widgets.append(progressbar.Bar())
widgets.append(' ')
widgets.append(progressbar.ETA())
p_bar = progressbar.ProgressBar(maxval=max_am, widgets=widgets)
p_bar.start()
try:
yield p_bar
finally:
p_bar.finish()
@contextlib.contextmanager
def tempdir():
# This seems like it was only added in python 3.2
# Make it since its useful...
# See: http://bugs.python.org/file12970/tempdir.patch
tdir = tempfile.mkdtemp()
try:
yield tdir
finally:
sh.deldir(tdir)
def versionize(input_version):
segments = input_version.split(".")
cleaned_segments = list()
for piece in segments:
piece = piece.strip()
if len(piece) == 0:
msg = "Disallowed empty version segment found"
raise ValueError(msg)
piece = piece.strip("*")
if len(piece) == 0:
cleaned_segments.append(STAR_VERSION)
elif ALL_NUMS.match(piece):
cleaned_segments.append(int(piece))
else:
piece_match = START_NUMS.match(piece)
if not piece_match:
msg = "Unknown version identifier %s" % (piece)
raise ValueError(msg)
else:
cleaned_segments.append(int(piece_match.group(1)))
if not cleaned_segments:
msg = "Disallowed empty version found"
raise ValueError(msg)
num_parts = [str(p) for p in cleaned_segments]
return distutils.version.LooseVersion(".".join(num_parts))
def sort_versions(versions, descending=True):
if not versions:
return list()
version_cleaned = list()
for v in versions:
version_cleaned.append(versionize(v))
versions_sorted = sorted(version_cleaned)
if not descending:
versions_sorted.reverse()
return versions_sorted
def get_host_ip():
"""
Returns the actual ip of the local machine.
This code figures out what source address would be used if some traffic
were to be sent out to some well known address on the Internet. In this
case, a private address is used, but the specific address does not
matter much. No traffic is actually sent.
Adjusted from nova code...
"""
ip = None
try:
csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
csock.connect((IP_LOOKER, 80))
(addr, _) = csock.getsockname()
csock.close()
ip = addr
except socket.error:
pass
# Ettempt to find it
if not ip:
interfaces = get_interfaces()
for (_, net_info) in interfaces.items():
ip_info = net_info.get(DEF_IP_VERSION)
if ip_info:
a_ip = ip_info.get('addr')
if a_ip:
ip = a_ip
break
# Just return a localhost version then
if not ip:
ip = DEF_IP
return ip
def is_interface(intfc):
return intfc in get_interfaces()
def get_interfaces():
interfaces = dict()
for intfc in netifaces.interfaces():
interface_info = dict()
interface_addresses = netifaces.ifaddresses(intfc)
ip6 = interface_addresses.get(netifaces.AF_INET6)
if ip6:
# Just take the first
interface_info[settings.IPV6] = ip6[0]
ip4 = interface_addresses.get(netifaces.AF_INET)
if ip4:
# Just take the first
interface_info[settings.IPV4] = ip4[0]
# Note: there are others but this is good for now..
interfaces[intfc] = interface_info
return interfaces
def format_secs_taken(secs):
output = "%.03f seconds" % (secs)
output += " or %.02f minutes" % (secs / 60.0)
return output
def joinlinesep(*pieces):
return os.linesep.join(pieces)
def param_replace_list(values, replacements, ignore_missing=False):
new_values = list()
if not values:
return new_values
for v in values:
if v is not None:
new_values.append(param_replace(str(v), replacements, ignore_missing))
return new_values
def find_params(text):
params_found = set()
if not text:
return params_found
def finder(match):
org_txt = match.group(0)
# Check if it's a comment, if so just return what it was and ignore
# any tokens that were there
if org_txt.startswith("#"):
return org_txt
param_name = match.group(1)
if param_name not in params_found:
params_found.add(param_name)
return org_txt
PARAM_SUB_REGEX.sub(finder, text)
return params_found
def param_replace(text, replacements, ignore_missing=False):
if not replacements:
replacements = dict()
if not text:
return ""
if ignore_missing:
LOG.debug("Performing parameter replacements (ignoring missing) on text %r" % (text))
else:
LOG.debug("Performing parameter replacements (not ignoring missing) on text %r" % (text))
possible_params = find_params(text)
LOG.debug("Possible replacements are: %r" % (", ".join(possible_params)))
LOG.debug("Given substitutions are: %s" % (replacements))
def replacer(match):
org_txt = match.group(0)
param_name = match.group(1)
# Check if it's a comment,
# if so just return what it was and ignore
# any tokens that were there
if org_txt.startswith('#'):
return org_txt
replacer = replacements.get(param_name)
if replacer is None and ignore_missing:
replacer = org_txt
elif replacer is None and not ignore_missing:
msg = "No replacement found for parameter %r" % (org_txt)
raise excp.NoReplacementException(msg)
else:
replacer = str(replacer)
LOG.debug("Replacing %r with %r" % (org_txt, replacer))
return replacer
replaced_text = PARAM_SUB_REGEX.sub(replacer, text)
LOG.debug("Replacement/s resulted in text %r" % (replaced_text))
return replaced_text
def _get_welcome_stack():
possibles = list()
# Thank you figlet ;)
# See: http://www.figlet.org/
possibles.append(r'''
___ ____ _____ _ _ ____ _____ _ ____ _ __
/ _ \| _ \| ____| \ | / ___|_ _|/ \ / ___| |/ /
| | | | |_) | _| | \| \___ \ | | / _ \| | | ' /
| |_| | __/| |___| |\ |___) || |/ ___ \ |___| . \
\___/|_| |_____|_| \_|____/ |_/_/ \_\____|_|\_\
''')
possibles.append(r'''
___ ___ ___ _ _ ___ _____ _ ___ _ __
/ _ \| _ \ __| \| / __|_ _/_\ / __| |/ /
| (_) | _/ _|| .` \__ \ | |/ _ \ (__| ' <
\___/|_| |___|_|\_|___/ |_/_/ \_\___|_|\_\
''')
possibles.append(r'''
____ ___ ____ _ _ ____ ___ ____ ____ _ _
| | |__] |___ |\ | [__ | |__| | |_/
|__| | |___ | \| ___] | | | |___ | \_
''')
possibles.append(r'''
_ ___ ___ _ _ __ ___ _ __ _ _
/ \| o \ __|| \| |/ _||_ _|/ \ / _|| |//
( o ) _/ _| | \\ |\_ \ | || o ( (_ | (
\_/|_| |___||_|\_||__/ |_||_n_|\__||_|\\
''')
possibles.append(r'''
_ ___ ___ _ __ ___ _____ _ __ _
,' \ / o |/ _/ / |/ /,' _//_ _/.' \ ,'_/ / //7
/ o |/ _,'/ _/ / || /_\ `. / / / o // /_ / ,'
|_,'/_/ /___//_/|_//___,' /_/ /_n_/ |__//_/\\
''')
possibles.append(r'''
_____ ___ ___ _ _ ___ _____ _____ ___ _ _
( _ )( _`\ ( _`\ ( ) ( )( _`\(_ _)( _ )( _`\ ( ) ( )
| ( ) || |_) )| (_(_)| `\| || (_(_) | | | (_) || ( (_)| |/'/'
| | | || ,__/'| _)_ | , ` |`\__ \ | | | _ || | _ | , <
| (_) || | | (_( )| |`\ |( )_) | | | | | | || (_( )| |\`\
(_____)(_) (____/'(_) (_)`\____) (_) (_) (_)(____/'(_) (_)
''')
return random.choice(possibles).strip("\n\r")
def center_text(text, fill, max_len):
return '{0:{fill}{align}{size}}'.format(text, fill=fill, align="^", size=max_len)
def _welcome_slang():
potentials = list()
potentials.append("And now for something completely different!")
return random.choice(potentials)
def color_text(text, color, bold=False,
underline=False, blink=False,
always_color=False):
text_attrs = list()
if bold:
text_attrs.append('bold')
if underline:
text_attrs.append('underline')
if blink:
text_attrs.append('blink')
if sh.in_terminal() or always_color:
return termcolor.colored(text, color, attrs=text_attrs)
else:
return text
def _color_blob(text, text_color):
def replacer(match):
contents = match.group(1)
return color_text(contents, text_color)
return MONTY_PYTHON_TEXT_RE.sub(replacer, text)
def _goodbye_header(worked):
# Cowsay headers
# See: http://www.nog.net/~tony/warez/cowsay.shtml
potentials_oks = list()
potentials_oks.append(r'''
___________
/ You shine \
| out like |
| a shaft |
| of gold |
| when all |
| around is |
\ dark. /
-----------
''')
potentials_oks.append(r'''
______________________________
< I'm a lumberjack and I'm OK. >
------------------------------
''')
potentials_oks.append(r'''
____________________
/ Australia! \
| Australia! |
| Australia! |
\ We love you, amen. /
--------------------
''')
potentials_oks.append(r'''
______________
/ Say no more, \
| Nudge nudge |
\ wink wink. /
--------------
''')
potentials_oks.append(r'''
________________
/ And there was \
\ much rejoicing /
----------------
''')
potentials_oks.append(r'''
__________
< Success! >
----------''')
potentials_fails = list()
potentials_fails.append(r'''
__________
< Failure! >
----------
''')
potentials_fails.append(r'''
___________
< Run away! >
-----------
''')
potentials_fails.append(r'''
______________________
/ NOBODY expects the \
\ Spanish Inquisition! /
----------------------
''')
potentials_fails.append(r'''
______________________
/ Spam spam spam spam \
\ baked beans and spam /
----------------------
''')
potentials_fails.append(r'''
____________________
/ Brave Sir Robin \
\ ran away. /
--------------------
''')
potentials_fails.append(r'''
_______________________
< Message for you, sir. >
-----------------------
''')
potentials_fails.append(r'''
____________________
/ We are the knights \
\ who say.... NI! /
--------------------
''')
potentials_fails.append(r'''
____________________
/ Now go away or I \
| shall taunt you a |
\ second time. /
--------------------
''')
potentials_fails.append(r'''
____________________
/ It's time for the \
| penguin on top of |
| your television to |
\ explode. /
--------------------
''')
potentials_fails.append(r'''
_____________________
/ We were in the nick \
| of time. You were |
\ in great peril. /
---------------------
''')
potentials_fails.append(r'''
___________________
/ I know a dead \
| parrot when I see |
| one, and I'm |
| looking at one |
\ right now. /
-------------------
''')
potentials_fails.append(r'''
_________________
/ Welcome to the \
| National Cheese |
\ Emporium /
-----------------
''')
potentials_fails.append(r'''
______________________
/ What is the airspeed \
| velocity of an |
\ unladen swallow? /
----------------------
''')
potentials_fails.append(r'''
______________________
/ Now stand aside, \
\ worthy adversary. /
----------------------
''')
potentials_fails.append(r'''
___________________
/ Okay, we'll call \
\ it a draw. /
-------------------
''')
potentials_fails.append(r'''
_______________
/ She turned me \
\ into a newt! /
---------------
''')
potentials_fails.append(r'''
___________________
< Fetchez la vache! >
-------------------
''')
potentials_fails.append(r'''
__________________________
/ We'd better not risk \
| another frontal assault, |
\ that rabbit's dynamite. /
--------------------------
''')
potentials_fails.append(r'''
______________________
/ This is supposed to \
| be a happy occasion. |
| Let's not bicker and |
| argue about who |
\ killed who. /
----------------------
''')
potentials_fails.append(r'''
_______________________
< You have been borked. >
-----------------------
''')
potentials_fails.append(r'''
__________________
/ We used to dream \
| of living in a |
\ corridor! /
-------------------
''')
if not worked:
msg = random.choice(potentials_fails).strip("\n\r")
colored_msg = _color_blob(msg, 'red')
else:
msg = random.choice(potentials_oks).strip("\n\r")
colored_msg = _color_blob(msg, 'green')
return colored_msg
def goodbye(worked):
if worked:
cow = COWS['happy']
eye_fmt = color_text('o', 'green')
ear = color_text("^", 'green')
else:
cow = COWS['unhappy']
eye_fmt = color_text("o", 'red')
ear = color_text("v", 'red')
cow = cow.strip("\n\r")
header = _goodbye_header(worked)
msg = cow.format(eye=eye_fmt, ear=ear,
header=header)
print(msg)
def welcome(ident):
lower = "| %s %s |" % (ident, version.version_string())
welcome_header = _get_welcome_stack()
max_line_len = len(max(welcome_header.splitlines(), key=len))
footer = color_text(settings.PROG_NICE_NAME, 'green')
footer += ": "
footer += color_text(lower, 'blue', True)
uncolored_footer = (settings.PROG_NICE_NAME + ": " + lower)
if max_line_len - len(uncolored_footer) > 0:
# This format string will center the uncolored text which
# we will then replace with the color text equivalent.
centered_str = center_text(uncolored_footer, " ", max_line_len)
footer = centered_str.replace(uncolored_footer, footer)
print(welcome_header)
print(footer)
real_max = max(max_line_len, len(uncolored_footer))
slang = center_text(_welcome_slang(), ' ', real_max)
print(color_text(slang, 'magenta', bold=True))
return ("-", real_max)