443 lines
14 KiB
Python
443 lines
14 KiB
Python
#
|
|
# Copyright (c) 2014-2020 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
"""
|
|
Utilities
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import yaml
|
|
|
|
import re
|
|
import six
|
|
|
|
import netaddr
|
|
from tsconfig import tsconfig
|
|
|
|
from controllerconfig.common import constants
|
|
from controllerconfig.common.exceptions import ValidateFail
|
|
from oslo_log import log
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
DEVNULL = open(os.devnull, 'w')
|
|
|
|
|
|
def start_service(name):
|
|
""" Start a systemd service """
|
|
try:
|
|
subprocess.check_call(["systemctl", "start", name], stdout=DEVNULL)
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to start %s service" % name)
|
|
raise
|
|
|
|
|
|
def stop_service(name):
|
|
""" Stop a systemd service """
|
|
try:
|
|
subprocess.check_call(["systemctl", "stop", name], stdout=DEVNULL)
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to stop %s service" % name)
|
|
raise
|
|
|
|
|
|
def restart_service(name):
|
|
""" Restart a systemd service """
|
|
try:
|
|
subprocess.check_call(["systemctl", "restart", name], stdout=DEVNULL)
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to restart %s service" % name)
|
|
raise
|
|
|
|
|
|
def check_sm_service(service, state):
|
|
""" Check whether an SM service has the supplied state """
|
|
try:
|
|
output = subprocess.check_output(["sm-query", "service", service])
|
|
return state in output
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
|
|
def wait_sm_service(service, timeout=180):
|
|
""" Check whether an SM service has been enabled.
|
|
:param service: SM service name
|
|
:param timeout: timeout in seconds
|
|
:return True if the service is enabled, False otherwise
|
|
"""
|
|
for _ in range(timeout):
|
|
if check_sm_service(service, 'enabled-active'):
|
|
return True
|
|
time.sleep(1)
|
|
return False
|
|
|
|
|
|
def get_address_from_hosts_file(hostname):
|
|
"""
|
|
Get the IP address of a host from the /etc/hosts file
|
|
:param hostname: hostname to look up
|
|
:return: IP address of host
|
|
"""
|
|
hosts = open('/etc/hosts')
|
|
for line in hosts:
|
|
if line.strip() and line.split()[1] == hostname:
|
|
return line.split()[0]
|
|
raise Exception("Hostname %s not found in /etc/hosts" % hostname)
|
|
|
|
|
|
def write_simplex_flag():
|
|
""" Write simplex flag. """
|
|
simplex_flag = "/etc/platform/simplex"
|
|
try:
|
|
open(simplex_flag, 'w')
|
|
except IOError:
|
|
LOG.error("Failed to open file: %s", simplex_flag)
|
|
raise Exception("Failed to write configuration file")
|
|
|
|
|
|
def create_manifest_runtime_config(filename, config):
|
|
"""Write the runtime Puppet configuration to a runtime file."""
|
|
if not config:
|
|
return
|
|
try:
|
|
with open(filename, 'w') as f:
|
|
yaml.dump(config, f, default_flow_style=False)
|
|
except Exception:
|
|
LOG.exception("failed to write config file: %s" % filename)
|
|
raise
|
|
|
|
|
|
def apply_manifest(controller_address_0, personality, manifest, hieradata,
|
|
stdout_progress=False, runtime_filename=None):
|
|
"""Apply puppet manifest files."""
|
|
|
|
# FIXME(mpeters): remove once manifests and modules are not dependent
|
|
# on checking the primary config condition
|
|
os.environ["INITIAL_CONFIG_PRIMARY"] = "true"
|
|
|
|
cmd = [
|
|
"/usr/local/bin/puppet-manifest-apply.sh",
|
|
hieradata,
|
|
str(controller_address_0),
|
|
personality,
|
|
manifest
|
|
]
|
|
|
|
if runtime_filename:
|
|
cmd.append((runtime_filename))
|
|
|
|
logfile = "/tmp/apply_manifest.log"
|
|
try:
|
|
with open(logfile, "w") as flog:
|
|
subprocess.check_call(cmd, stdout=flog, stderr=flog)
|
|
except subprocess.CalledProcessError:
|
|
msg = "Failed to execute %s manifest" % manifest
|
|
print(msg)
|
|
raise Exception(msg)
|
|
|
|
|
|
def create_system_config():
|
|
cmd = ["/usr/bin/sysinv-puppet",
|
|
"create-system-config",
|
|
constants.HIERADATA_PERMDIR]
|
|
try:
|
|
subprocess.check_call(cmd)
|
|
except subprocess.CalledProcessError:
|
|
msg = "Failed to update puppet hiera system config"
|
|
print(msg)
|
|
raise Exception(msg)
|
|
|
|
|
|
def create_host_config(hostname=None):
|
|
cmd = ["/usr/bin/sysinv-puppet",
|
|
"create-host-config",
|
|
constants.HIERADATA_PERMDIR]
|
|
if hostname:
|
|
cmd.append(hostname)
|
|
|
|
try:
|
|
subprocess.check_call(cmd)
|
|
except subprocess.CalledProcessError:
|
|
msg = "Failed to update puppet hiera host config"
|
|
print(msg)
|
|
raise Exception(msg)
|
|
|
|
|
|
def persist_config():
|
|
"""Copy temporary config files into new DRBD filesystem"""
|
|
|
|
# Persist temporary keyring
|
|
try:
|
|
if os.path.isdir(constants.KEYRING_WORKDIR):
|
|
shutil.move(constants.KEYRING_WORKDIR, constants.KEYRING_PERMDIR)
|
|
except IOError:
|
|
LOG.error("Failed to persist temporary keyring")
|
|
raise Exception("Failed to persist temporary keyring")
|
|
|
|
# Move puppet working files into permanent directory
|
|
try:
|
|
# ensure parent directory is present
|
|
subprocess.call(["mkdir", "-p", tsconfig.PUPPET_PATH])
|
|
|
|
# move hiera data to puppet directory
|
|
if os.path.isdir(constants.HIERADATA_WORKDIR):
|
|
subprocess.check_call(["mv", constants.HIERADATA_WORKDIR,
|
|
tsconfig.PUPPET_PATH])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to persist puppet config files")
|
|
raise Exception("Failed to persist puppet config files")
|
|
|
|
# Move config working files into permanent directory
|
|
try:
|
|
# ensure parent directory is present
|
|
subprocess.call(["mkdir", "-p",
|
|
os.path.dirname(constants.CONFIG_PERMDIR)])
|
|
|
|
if os.path.isdir(constants.CONFIG_WORKDIR):
|
|
# Remove destination directory in case it was created previously
|
|
subprocess.call(["rm", "-rf", constants.CONFIG_PERMDIR])
|
|
|
|
# move working data to config directory
|
|
subprocess.check_call(["mv", constants.CONFIG_WORKDIR,
|
|
constants.CONFIG_PERMDIR])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to persist config files")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
# Copy postgres config files for mate
|
|
try:
|
|
subprocess.check_call(["mkdir",
|
|
constants.CONFIG_PERMDIR + "/postgresql"])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to create postgresql dir")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
try:
|
|
for f in glob.glob("/etc/postgresql/*.conf"):
|
|
subprocess.check_call([
|
|
"cp", "-p", f, constants.CONFIG_PERMDIR + "/postgresql/"])
|
|
except IOError:
|
|
LOG.error("Failed to persist postgresql config files")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
# Set up replicated directory for PXE config files
|
|
try:
|
|
subprocess.check_call([
|
|
"mkdir", "-p", constants.CONFIG_PERMDIR + "/pxelinux.cfg"])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to create persistent pxelinux.cfg directory")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
try:
|
|
subprocess.check_call(["ln", "-s", constants.CONFIG_PERMDIR +
|
|
"/pxelinux.cfg", "/pxeboot/pxelinux.cfg"])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to create pxelinux.cfg symlink")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
# Copy branding tarball for mate
|
|
if os.listdir('/opt/branding'):
|
|
try:
|
|
subprocess.check_call([
|
|
"mkdir", constants.CONFIG_PERMDIR + "/branding"])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to create branding dir")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
try:
|
|
for f in glob.glob("/opt/branding/*.tgz"):
|
|
subprocess.check_call([
|
|
"cp", "-p", f, constants.CONFIG_PERMDIR + "/branding/"])
|
|
break
|
|
except IOError:
|
|
LOG.error("Failed to persist branding config files")
|
|
raise Exception("Failed to persist config files")
|
|
|
|
|
|
def apply_banner_customization():
|
|
""" Apply and Install banners provided by the user """
|
|
""" execute: /usr/sbin/apply_banner_customization """
|
|
logfile = "/tmp/apply_banner_customization.log"
|
|
try:
|
|
with open(logfile, "w") as blog:
|
|
subprocess.check_call(["/usr/sbin/apply_banner_customization",
|
|
"/opt/banner"],
|
|
stdout=blog, stderr=blog)
|
|
except subprocess.CalledProcessError:
|
|
error_text = "Failed to apply banner customization"
|
|
print("%s; see %s for detail" % (error_text, logfile))
|
|
|
|
|
|
def mtce_restart():
|
|
"""Restart maintenance processes to handle interface changes"""
|
|
restart_service("mtcClient")
|
|
restart_service("hbsClient")
|
|
restart_service("pmon")
|
|
|
|
|
|
def mark_config_complete():
|
|
"""Signal initial configuration has been completed"""
|
|
try:
|
|
subprocess.check_call(["touch",
|
|
constants.INITIAL_CONFIG_COMPLETE_FILE])
|
|
subprocess.call(["rm", "-rf", constants.KEYRING_WORKDIR])
|
|
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to mark initial config complete")
|
|
raise Exception("Failed to mark initial config complete")
|
|
|
|
|
|
def configure_hostname(hostname):
|
|
"""Configure hostname for this host."""
|
|
|
|
hostname_file = '/etc/hostname'
|
|
try:
|
|
with open(hostname_file, 'w') as f:
|
|
f.write(hostname + "\n")
|
|
except IOError:
|
|
LOG.error("Failed to update file: %s", hostname_file)
|
|
raise Exception("Failed to configure hostname")
|
|
|
|
try:
|
|
subprocess.check_call(["hostname", hostname])
|
|
except subprocess.CalledProcessError:
|
|
LOG.error("Failed to update hostname %s" % hostname)
|
|
raise Exception("Failed to configure hostname")
|
|
|
|
|
|
def touch(fname):
|
|
with open(fname, 'a'):
|
|
os.utime(fname, None)
|
|
|
|
|
|
def is_ssh_parent():
|
|
"""Determine if current process is started from a ssh session"""
|
|
command = ('pstree -s %d' % (os.getpid()))
|
|
try:
|
|
cmd_output = subprocess.check_output(command, shell=True)
|
|
if "ssh" in cmd_output:
|
|
return True
|
|
else:
|
|
return False
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
|
|
def is_valid_mac(mac):
|
|
"""Verify the format of a MAC addres."""
|
|
if not mac:
|
|
return False
|
|
m = "[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$"
|
|
return isinstance(mac, six.string_types) and re.match(m, mac.lower())
|
|
|
|
|
|
def validate_network_str(network_str, minimum_size,
|
|
existing_networks=None, multicast=False):
|
|
"""Determine whether a network is valid."""
|
|
try:
|
|
network = netaddr.IPNetwork(network_str)
|
|
if network.size < minimum_size:
|
|
raise ValidateFail("Subnet too small - must have at least %d "
|
|
"addresses" % minimum_size)
|
|
elif network.version == 6 and network.prefixlen < 64:
|
|
raise ValidateFail("IPv6 minimum prefix length is 64")
|
|
elif existing_networks:
|
|
if any(network.ip in subnet for subnet in existing_networks):
|
|
raise ValidateFail("Subnet overlaps with another "
|
|
"configured subnet")
|
|
elif multicast and not network.is_multicast():
|
|
raise ValidateFail("Invalid subnet - must be multicast")
|
|
return network
|
|
except netaddr.AddrFormatError:
|
|
raise ValidateFail(
|
|
"Invalid subnet - not a valid IP subnet")
|
|
|
|
|
|
def is_valid_domain(url_str):
|
|
r = re.compile(
|
|
r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)' # domain...
|
|
r'+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
|
|
r'[A-Za-z0-9-_]*)' # localhost, hostname
|
|
r'(?::\d+)?' # optional port
|
|
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
|
|
|
url = r.match(url_str)
|
|
if url:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def validate_address_str(ip_address_str, network):
|
|
"""Determine whether an address is valid."""
|
|
try:
|
|
ip_address = netaddr.IPAddress(ip_address_str)
|
|
if ip_address.version != network.version:
|
|
msg = ("Invalid IP version - must match network version " +
|
|
ip_version_to_string(network.version))
|
|
raise ValidateFail(msg)
|
|
elif ip_address == network:
|
|
raise ValidateFail("Cannot use network address")
|
|
elif ip_address == network.broadcast:
|
|
raise ValidateFail("Cannot use broadcast address")
|
|
elif ip_address not in network:
|
|
raise ValidateFail(
|
|
"Address must be in subnet %s" % str(network))
|
|
return ip_address
|
|
except netaddr.AddrFormatError:
|
|
raise ValidateFail(
|
|
"Invalid address - not a valid IP address")
|
|
|
|
|
|
def ip_version_to_string(ip_version):
|
|
"""Determine whether a nameserver address is valid."""
|
|
if ip_version == 4:
|
|
return "IPv4"
|
|
elif ip_version == 6:
|
|
return "IPv6"
|
|
else:
|
|
return "IP"
|
|
|
|
|
|
def is_subcloud():
|
|
conn = psycopg2.connect("dbname='sysinv' user='postgres'")
|
|
with conn:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute("SELECT * from i_system")
|
|
system = cur.fetchone()
|
|
return system['distributed_cloud_role'] == 'subcloud'
|
|
|
|
|
|
def get_keystone_user_id(user_name):
|
|
""" Get the a keystone user id by name"""
|
|
|
|
conn = psycopg2.connect("dbname='keystone' user='postgres'")
|
|
with conn:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute("SELECT user_id FROM local_user WHERE name=%s" %
|
|
user_name)
|
|
user_id = cur.fetchone()
|
|
return user_id['user_id']
|
|
|
|
|
|
def get_keystone_project_id(project_name):
|
|
""" Get the a keystone project id by name"""
|
|
|
|
conn = psycopg2.connect("dbname='keystone' user='postgres'")
|
|
with conn:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute("SELECT id FROM project WHERE name=%s" %
|
|
project_name)
|
|
project_id = cur.fetchone()
|
|
return project_id['id']
|