# Copyright (c) 2015, 2016, Oracle and/or its affiliates. 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.
""" openstack_upgrade - common functions used by the various OpenStack
components to facilitate upgrading of configuration files and MySQL
databases/tables (if in use)
Wrapper functions around ovs-vsctl(8) command used by Nova and Neutron to
determine the lower-link needed for VNIC creation.
"""
import collections
from ConfigParser import NoOptionError
from datetime import datetime
import errno
import glob
import json
import os
import shutil
from subprocess import CalledProcessError, Popen, PIPE, check_call
import time
import uuid
import iniparse
OVS_VSCTL_TIMEOUT = "10"
def create_backups(directory):
""" create backups of each configuration file which also has a .new file
from the upgrade.
"""
today = datetime.now().strftime("%Y%m%d%H%M%S")
cwd = os.getcwd()
os.chdir(directory)
for new_file in glob.glob('*.new'):
# copy the old conf file to a backup
old_file = new_file.replace('.new', '')
try:
shutil.copy2(old_file, old_file + '.' + today)
except (IOError, OSError):
print 'unable to create a backup of %s' % old_file
os.chdir(cwd)
def update_mapping(section, key, mapping):
""" look for deprecated variables and, if found, convert it to the new
section/key.
"""
if (section, key) in mapping:
print "Deprecated value found: [%s] %s" % (section, key)
section, key = mapping[(section, key)]
if section is None and key is None:
print "Removing from configuration"
else:
print "Updating to: [%s] %s" % (section, key)
return section, key
def alter_mysql_tables(engine):
""" Convert MySQL tables to use utf8
"""
import MySQLdb
for _none in range(60):
try:
db = MySQLdb.connect(host=engine.url.host,
user=engine.url.username,
passwd=engine.url.password,
db=engine.url.database)
break
except MySQLdb.OperationalError as err:
# mysql is not ready. sleep for 2 more seconds
time.sleep(2)
else:
print "Unable to connect to MySQL: %s" % err
print ("Please verify MySQL is properly configured and online "
"before using svcadm(1M) to clear this service.")
raise RuntimeError
cursor = db.cursor()
cursor.execute("SHOW table status")
cursor.execute("ALTER DATABASE %s CHARACTER SET = 'utf8'" %
engine.url.database)
cursor.execute("ALTER DATABASE %s COLLATE = 'utf8_general_ci'" %
engine.url.database)
cursor.execute("SHOW tables")
res = cursor.fetchall()
if res:
cursor.execute("SET foreign_key_checks = 0")
for item in res:
cursor.execute("ALTER TABLE %s.%s CONVERT TO "
"CHARACTER SET 'utf8', COLLATE 'utf8_general_ci'"
% (engine.url.database, item[0]))
cursor.execute("SET foreign_key_checks = 1")
db.commit()
db.close()
def modify_conf(old_file, mapping=None, exception_list=None):
""" Copy over all uncommented options from the old configuration file. In
addition, look for deprecated section/keys and convert them to the new
section/key.
"""
if not os.path.exists(old_file):
return
new_file = old_file + '.new'
# open the previous version
old = iniparse.ConfigParser()
old.readfp(open(old_file))
# open the new version
new = iniparse.ConfigParser()
try:
new.readfp(open(new_file))
except IOError as err:
if err.errno == errno.ENOENT:
# The upgrade did not deliver a .new file so, return
print "%s not found - continuing with %s" % (new_file, old_file)
return
else:
raise
print "\nupdating %s" % old_file
# walk every single section for uncommented options
default_items = set(old.items('DEFAULT'))
for old_section in old.sections() + ['DEFAULT']:
# DEFAULT items show up in every section so remove them
if old_section != 'DEFAULT':
section_items = set(old.items(old_section)) - default_items
else:
section_items = default_items
for old_key, value in section_items:
# Look for deprecated section/keys
if mapping is not None:
new_section, new_key = update_mapping(old_section, old_key,
mapping)
if new_section is None and new_key is None:
# option is deprecated so continue
continue
else:
# no deprecated values for this file so just copy the values
# over
new_section, new_key = old_section, old_key
# Look for exceptions
if exception_list:
if (new_section, new_key) in exception_list:
if (new_section != 'DEFAULT' and
not new.has_section(new_section)):
new.add_section(new_section)
print "Preserving [%s] %s = %s" % \
(new_section, new_key, value)
new.set(new_section, new_key, value)
continue
if new_section != 'DEFAULT' and not new.has_section(new_section):
new.add_section(new_section)
# print to the log when a value for old_section.old_key is changing
# to a new value
try:
new_value = new.get(new_section, new_key)
if new_value != value and '%SERVICE' not in new_value:
print "Changing [%s] %s:\n- %s\n+ %s" % \
(old_section, old_key, value, new_value)
print
except NoOptionError:
# the new configuration file does not have this option set so
# just continue
pass
# Only copy the old value to the new conf file if the entry doesn't
# exist in the new file or if it contains '%SERVICE'
if not new.has_option(new_section, new_key) or \
'%SERVICE' in new.get(new_section, new_key):
new.set(new_section, new_key, value)
# copy the new conf file in place
with open(old_file, 'wb+') as fh:
new.write(fh)
def move_conf(original_file, new_file, mapping):
""" move each entry in mapping from the original file to the new file.
"""
# open the original file
original = iniparse.ConfigParser()
original.readfp(open(original_file))
# open the new file
new = iniparse.ConfigParser()
new.readfp(open(new_file))
# The mappings dictionary look similar to the deprecation mappings:
# (original_section, original_key): (new_section, new_key)
for (original_section, original_key) in mapping:
try:
original_value = original.get(original_section, original_key)
except NoOptionError:
# the original file does not contain this mapping so continue
continue
new_section, new_key = mapping.get((original_section, original_key))
if new_section != 'DEFAULT' and not new.has_section(new_section):
new.add_section(new_section)
print 'Moving [%s] %s from %s to [%s] %s in %s' % \
(original_section, original_key, original_file,
new_section, new_key, new_file)
# set the option in the new file
new.set(new_section, new_key, original_value)
# remove the option from the old file
original.remove_option(original_section, original_key)
with open(original_file, 'wb+') as fh:
original.write(fh)
with open(new_file, 'wb+') as fh:
new.write(fh)
def _val_to_py(val):
"""Convert a json ovsdb return value to native python object"""
if isinstance(val, collections.Sequence) and len(val) == 2:
if val[0] == "uuid":
return uuid.UUID(val[1])
elif val[0] == "set":
return [_val_to_py(x) for x in val[1]]
elif val[0] == "map":
return {_val_to_py(x): _val_to_py(y) for x, y in val[1]}
return val
def get_ovsdb_info(table, columns=None):
"""Return a list of dictionaries where-in each dictionary captures the
requested (or all) column name and its value"""
cmd = ["/usr/sbin/ovs-vsctl", "-t", OVS_VSCTL_TIMEOUT, "-f", "json"]
if columns:
cmd.extend(["--columns", ",".join(columns)])
cmd.append("list")
cmd.append(table)
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if p.returncode != 0:
raise RuntimeError("Error running '%s': %s" % (" ".join(cmd), error))
output = output.strip().replace(r'\\', '\\')
try:
ovsinfo = json.loads(output)
except Exception as err:
raise RuntimeError("Failed to load %s as json: %s" % (output, err))
headings = ovsinfo['headings']
data = ovsinfo['data']
results = []
for record in data:
obj = {}
for pos, heading in enumerate(headings):
obj[heading] = _val_to_py(record[pos])
results.append(obj)
return results
def kill_contract(attempts, interval, ctid):
""" Keeps issuing SIGTERM to contract-id at specified intervals until
either the contract is empty or the specified number of attempts are made.
Returns 0 if pkill failed, 1 if contract was successfully emptied and 2
if attempts were exhausted before the contract could be emptied.
"""
for _ in xrange(attempts):
# Kill the SMF contract
try:
check_call(["/usr/bin/pkill", "-c", ctid])
except CalledProcessError as err:
print "failed to kill the SMF contract: %s" % err
return 0
time.sleep(interval)
try:
# check if contract is empty
check_call(["/usr/bin/pgrep", "-c", ctid], stdout=PIPE,
stderr=PIPE)
except:
# contract is empty
return 1
return 2