523 lines
22 KiB
Python
523 lines
22 KiB
Python
"""Provides the capability to setup a StarlingX iso with specific
|
|
configuration"""
|
|
from ast import literal_eval
|
|
from imp import reload
|
|
import os
|
|
import re
|
|
from shutil import copyfile
|
|
from shutil import copytree
|
|
from shutil import rmtree
|
|
import sys
|
|
import threading
|
|
import pexpect
|
|
import yaml
|
|
|
|
from bash import bash
|
|
|
|
from Config import config
|
|
from Utils import logger
|
|
from Utils import network
|
|
from Utils.utils import isdir
|
|
|
|
# reloading config.ini
|
|
reload(config)
|
|
|
|
# Global variables
|
|
THIS_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_PATH = os.path.dirname(THIS_PATH)
|
|
PROMPT = '$'
|
|
|
|
# Setup the logger
|
|
LOG_FILENAME = 'iso_setup_baremetal.log'
|
|
LOG_PATH = config.get('general', 'LOG_PATH')
|
|
LOG = logger.setup_logging('iso_setup_baremetal',
|
|
log_file='{}/{}'.format(LOG_PATH, LOG_FILENAME),
|
|
console_log=False)
|
|
|
|
|
|
class PxeServer(object):
|
|
"""Handle PXE services and mount ISO for Installation"""
|
|
|
|
def __init__(self, iso_path):
|
|
self.iso_path = iso_path
|
|
self.iso_name = os.path.basename(self.iso_path).replace('.iso', '')
|
|
self.tftp_dir = '/var/lib/tftpboot/uefi'
|
|
|
|
def mount_iso(self):
|
|
"""Mounting ISO and grabbing shim, grub.efi and grub.cfg files"""
|
|
|
|
# Mounting ISO on /mnt and on http server
|
|
mount_point = '/mnt'
|
|
http_mnt_point = '/var/www/html/stx'
|
|
tmp_mnt_point = '/tmp'
|
|
|
|
if os.listdir(mount_point):
|
|
LOG.info('%s is busy umounting', mount_point)
|
|
umounting_attempts = 3
|
|
|
|
while umounting_attempts > 0:
|
|
umounting = bash('sudo umount -l {}'.format(mount_point))
|
|
|
|
if umounting.stderr and umounting_attempts:
|
|
LOG.info('Failed to umount %s, retrying...', mount_point)
|
|
elif umounting.stderr and not umounting_attempts:
|
|
LOG.info('Max umounting attempts reached, leaving '
|
|
'installation')
|
|
sys.exit(1)
|
|
else:
|
|
break
|
|
|
|
umounting_attempts -= 1
|
|
|
|
bash('sudo mount {0} {1}'.format(self.iso_path, mount_point))
|
|
LOG.info('Mounting ISO on %s', mount_point)
|
|
|
|
if isdir(os.path.join(http_mnt_point, self.iso_name)):
|
|
LOG.info('Folder %s/%s already exists in http server, deleting '
|
|
'it.', http_mnt_point, self.iso_name)
|
|
rmtree(os.path.join(http_mnt_point, self.iso_name))
|
|
copytree(mount_point, os.path.join(http_mnt_point, self.iso_name))
|
|
|
|
if isdir(os.path.join(tmp_mnt_point, self.iso_name)):
|
|
LOG.info('Folder %s/%s already exists in http server, deleting '
|
|
'it.', tmp_mnt_point, self.iso_name)
|
|
rmtree(os.path.join(tmp_mnt_point, self.iso_name))
|
|
|
|
# Changing from RPM to CPIO format
|
|
LOG.info('Uncompressing RPM necessary files')
|
|
copytree(os.path.join(http_mnt_point, self.iso_name, 'Packages'),
|
|
os.path.join(tmp_mnt_point, self.iso_name, 'Packages'))
|
|
grub2_regex = re.compile('grub2-efi-x64-[0-9]')
|
|
os.chdir(os.path.join(tmp_mnt_point, self.iso_name, 'Packages'))
|
|
|
|
for package in os.listdir(
|
|
os.path.join(tmp_mnt_point, self.iso_name, 'Packages')):
|
|
|
|
if 'shim' in package or grub2_regex.search(package):
|
|
LOG.info('Found grub/shim file uncompressing it')
|
|
bash('rpm2cpio {} | cpio -dimv'.format(package))
|
|
|
|
# Copying shim, and grub files to tftpboot folder
|
|
# fixme: handle condition to make sure tftp_dir exists
|
|
if not os.path.isdir(self.tftp_dir):
|
|
os.makedirs(self.tftp_dir)
|
|
LOG.info('Copying grub and shim files to TFTP server')
|
|
|
|
for root, _, files in os.walk('/tmp/{}/Packages'.format(
|
|
self.iso_name)):
|
|
|
|
for package in files:
|
|
if any(boot_file in package for boot_file in ('shim.efi',
|
|
'grubx64.efi')):
|
|
copyfile(os.path.join(root, package),
|
|
os.path.join(self.tftp_dir, package))
|
|
|
|
if 'grub.cfg' in package:
|
|
copyfile(os.path.join(root, package),
|
|
os.path.join(self.tftp_dir, package))
|
|
copyfile(os.path.join(http_mnt_point, self.iso_name,
|
|
'EFI/BOOT/grub.cfg'),
|
|
os.path.join(self.tftp_dir, 'grub.cfg'))
|
|
|
|
# Copying vmlinuz and initrd
|
|
images_dir = os.path.join(self.tftp_dir, 'images')
|
|
|
|
if isdir(images_dir):
|
|
LOG.info('%s already exists, deleting directory.', images_dir)
|
|
rmtree(images_dir)
|
|
|
|
LOG.info('Copying vmlinuz and initrd files.')
|
|
copytree(os.path.join(mount_point, 'images/pxeboot'),
|
|
os.path.join(self.tftp_dir, 'images'))
|
|
|
|
@staticmethod
|
|
def check_pxe_services():
|
|
"""This function is intended to restart DHCP service
|
|
DHCP service needs to be restarted in order to grab the changes on the
|
|
dhcp config file"""
|
|
|
|
LOG.info('Checking PXE needed services')
|
|
services = ['dhcpd', 'tftp', 'httpd']
|
|
|
|
for service in services:
|
|
active_service = bash('sudo systemctl is-active {}'
|
|
.format(service))
|
|
if 'active'.encode('utf-8') in active_service.stdout:
|
|
LOG.info('%s service is active', service)
|
|
continue
|
|
else:
|
|
LOG.info('%s service is not active, restarting', service)
|
|
bash('sudo systemctl restart {}'.format(service))
|
|
|
|
def get_efi_boot_line(self, grub_dict):
|
|
"""Get linuxefi command and initrdefi command from grub_dict
|
|
|
|
Get linuxefi command and initrdefi command from grub_dict according to
|
|
specified option on configuration argument while running runner.py
|
|
"""
|
|
configuration_type = config.get('general', 'CONFIGURATION_TYPE')
|
|
http_server_ip = config.get('baremetal', 'HTTP_SERVER')
|
|
LOG.info('config_type')
|
|
LOG.info(configuration_type)
|
|
boot_lines = dict()
|
|
|
|
if configuration_type == 'simplex':
|
|
boot_lines = grub_dict['aio']['serial']
|
|
elif configuration_type == 'duplex':
|
|
boot_lines = grub_dict['aio']['serial']
|
|
elif configuration_type == 'multinode_controller_storage':
|
|
boot_lines = grub_dict['standard']['serial']
|
|
elif configuration_type == 'multinode_dedicated_storage':
|
|
boot_lines = grub_dict['standard']['serial']
|
|
|
|
prefix = 'uefi/images'
|
|
linuxefi_cmd = boot_lines['linuxefi']
|
|
linuxefi_http_cmd = list()
|
|
|
|
for parameter in linuxefi_cmd.split(' '):
|
|
if 'inst.ks' in parameter:
|
|
ks_file = parameter.split('/')[-1]
|
|
parameter = 'inst.ks=http://{server}/stx/{iso}/{ks_file}' \
|
|
.format(server=http_server_ip, iso=self.iso_name,
|
|
ks_file=ks_file)
|
|
linuxefi_http_cmd.append(parameter)
|
|
elif 'inst.stage2' in parameter:
|
|
parameter = 'inst.stage2=http://{server}/stx/{iso}' \
|
|
.format(server=http_server_ip, iso=self.iso_name)
|
|
linuxefi_http_cmd.append(parameter)
|
|
elif 'vmlinuz' in parameter:
|
|
parameter = '{prefix}{parameter}'.format(prefix=prefix,
|
|
parameter=parameter)
|
|
linuxefi_http_cmd.append(parameter)
|
|
else:
|
|
linuxefi_http_cmd.append(parameter)
|
|
inst_repo = 'inst.repo=http://{server}/stx/{iso}'\
|
|
.format(server=http_server_ip, iso=self.iso_name)
|
|
linuxefi_http_cmd.append(inst_repo)
|
|
boot_lines['linuxefi'] = ' '.join(linuxefi_http_cmd)
|
|
|
|
initrd_cmd = boot_lines['initrdefi']
|
|
initrd_prefix_cmd = list()
|
|
|
|
for parameter in initrd_cmd.split(' '):
|
|
if 'initrd.img' in parameter:
|
|
parameter = '{prefix}{parameter}'.format(prefix=prefix,
|
|
parameter=parameter)
|
|
initrd_prefix_cmd.append(parameter)
|
|
else:
|
|
initrd_prefix_cmd.append(parameter)
|
|
boot_lines['initrdefi'] = ' '.join(initrd_prefix_cmd)
|
|
return boot_lines
|
|
|
|
def handle_grub(self):
|
|
"""Pointing source files to http server on grub"""
|
|
|
|
installation_type = config.get('general', 'CONFIGURATION_TYPE')
|
|
grub_dict = analyze_grub(
|
|
os.path.join(self.tftp_dir, 'grub.cfg'))
|
|
grub_lines = self.get_efi_boot_line(grub_dict)
|
|
grub_entry = ("menuentry '{config}'{{\n{linuxefi}\n{initrdefi}\n}}"
|
|
.format(config=installation_type,
|
|
linuxefi=grub_lines['linuxefi'],
|
|
initrdefi=grub_lines['initrdefi']))
|
|
grub_timeout = 'timeout=5\n'
|
|
with open(os.path.join(self.tftp_dir, 'grub.cfg'), 'w', encoding='utf8') as grub_file:
|
|
grub_file.writelines(grub_timeout)
|
|
grub_file.write(grub_entry)
|
|
|
|
|
|
class Node(object):
|
|
"""Constructs a Node server that can be booted to pxe and also follow the
|
|
installation of STX system
|
|
"""
|
|
|
|
def __init__(self, node):
|
|
self.name = node['name']
|
|
self.personality = node['personality']
|
|
self.pxe_nic_mac = node['pxe_nic_mac']
|
|
self.bmc_ip = node['bmc_ip']
|
|
self.bmc_user = node['bmc_user']
|
|
self.bmc_pswd = node['bmc_pswd']
|
|
if self.name == 'controller-0':
|
|
self.installation_ip = node['installation_ip']
|
|
|
|
def boot_server_to_pxe(self):
|
|
"""Boot the installation target server using PXE server"""
|
|
|
|
LOG.info('Booting %s To PXE', self.name)
|
|
LOG.info('Node %s : Setting PXE as first boot option', self.name)
|
|
set_pxe = bash('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'chassis bootdev pxe'.format(
|
|
node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
if set_pxe.stderr:
|
|
LOG.info(set_pxe.stderr)
|
|
|
|
LOG.info('Node %s : Resetting target.', self.name)
|
|
power_status = bash('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'chassis power status'.format(
|
|
node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
if power_status.stderr:
|
|
LOG.info(set_pxe.stderr)
|
|
|
|
if "Chassis Power is on" in str(power_status):
|
|
power = bash('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'chassis power reset'.format(
|
|
node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
else:
|
|
power = bash('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'chassis power on'.format(
|
|
node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
if power.stderr:
|
|
LOG.info(power.stderr)
|
|
|
|
LOG.info('Node %s: Deactivating sol sessions.', self.name)
|
|
kill_sol = bash('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} sol '
|
|
'deactivate'.format(node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
if kill_sol.stderr:
|
|
LOG.info(kill_sol.stderr)
|
|
|
|
def follow_node_installation(self):
|
|
"""This function is intended to follow nodes installation"""
|
|
|
|
user_name = config.get('credentials', 'STX_DEPLOY_USER_NAME')
|
|
password = config.get('credentials', 'STX_DEPLOY_USER_PSWD')
|
|
|
|
LOG.info('Node %s: Following node installation.', self.name)
|
|
installation = pexpect.spawn(('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'sol activate')
|
|
.format(node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
installation.logfile = open('{}/iso_setup_installation.txt'.format(
|
|
LOG_PATH), 'wb')
|
|
installation.timeout = int(config.get('iso_installer', 'BOOT_TIMEOUT'))
|
|
installation.expect('Start PXE over IPv4.')
|
|
LOG.info('Node %s: Trying to boot using PXE', self.name)
|
|
installation.expect('Linux version')
|
|
LOG.info('Node %s: Loading Linux Kernel', self.name)
|
|
installation.expect('Welcome to')
|
|
LOG.info('Node %s: CentOS have been loaded', self.name)
|
|
installation.expect('Starting installer, one moment...')
|
|
LOG.info('Node %s: Starting installer ...', self.name)
|
|
installation.expect('Performing post-installation setup tasks')
|
|
LOG.info('Node %s: Performing post-installation setup tasks',
|
|
self.name)
|
|
installation.expect('login:')
|
|
LOG.info('Node %s: the system boot up correctly', self.name)
|
|
LOG.info('Node %s: logging into the system', self.name)
|
|
installation.sendline(user_name)
|
|
installation.expect('Password:')
|
|
installation.sendline(user_name)
|
|
LOG.info('Node %s: setting a new password', self.name)
|
|
installation.expect('UNIX password:')
|
|
installation.sendline(user_name)
|
|
installation.expect('New password:')
|
|
installation.sendline(password)
|
|
installation.expect('Retype new password:')
|
|
installation.sendline(password)
|
|
installation.expect('$')
|
|
LOG.info('Node %s: the password was changed successfully', self.name)
|
|
installation.close()
|
|
LOG.info('Node %s: Closing SOL session after successfully '
|
|
'installation', self.name)
|
|
deactivate_sol = bash(('ipmitool -I lanplus -H {node_bmc_ip} '
|
|
'-U {node_bmc_user} -P {node_bmc_pswd} '
|
|
'sol deactivate')
|
|
.format(node_bmc_ip=self.bmc_ip,
|
|
node_bmc_user=self.bmc_user,
|
|
node_bmc_pswd=self.bmc_pswd))
|
|
if not deactivate_sol.stderr:
|
|
LOG.info('Node %s: SOL session closed successfully', self.name)
|
|
|
|
|
|
def analyze_grub(grub_cfg_file):
|
|
"""Get linuxefi command and initrdefi command from grub_dict
|
|
|
|
Get linuxefi command and initrdefi command from grub_dict according to
|
|
selected option in config file
|
|
"""
|
|
with open(grub_cfg_file, 'r', encoding='utf8') as grub:
|
|
lines = grub.readlines()
|
|
cmd_lines = list()
|
|
|
|
for line in lines:
|
|
|
|
if 'linuxefi' in line:
|
|
line = line.strip()
|
|
cmd_line = "'linuxefi': '{line}',".format(line=line)
|
|
cmd_lines.append(cmd_line)
|
|
elif 'initrdefi' in line:
|
|
line = line.strip()
|
|
cmd_line = "'initrdefi': '{line}'".format(line=line)
|
|
cmd_lines.append(cmd_line)
|
|
elif 'submenu' in line or 'menuentry' in line:
|
|
if re.search('--id=(.*) {', line):
|
|
menu_name = re.search('--id=(.*) {', line)
|
|
else:
|
|
menu_name = re.search("'(.*)'", line)
|
|
menu_name = menu_name.group(1)
|
|
line = "'{}': {{".format(menu_name)
|
|
cmd_lines.append(line)
|
|
elif '}' in line:
|
|
cmd_lines.append('},')
|
|
|
|
grub_menu = ''.join(cmd_lines) # type: str
|
|
grub_menu = '{{ {} }}'.format(grub_menu)
|
|
grub_dict = literal_eval(grub_menu)
|
|
return grub_dict
|
|
|
|
|
|
def mount_iso_on_pxe(iso):
|
|
""""Manage and enable PXE services"""
|
|
|
|
pxe_server = PxeServer(iso)
|
|
pxe_server.mount_iso()
|
|
pxe_server.handle_grub()
|
|
pxe_server.check_pxe_services()
|
|
|
|
|
|
def install_iso_master_controller():
|
|
"""Launch ISO installation on controller-0"""
|
|
|
|
nodes_file = os.path.join(os.environ['PYTHONPATH'], 'baremetal',
|
|
'baremetal_setup.yaml')
|
|
nodes = yaml.safe_load(open(nodes_file, encoding='utf8'))
|
|
|
|
# Update config.ini with OAM and MGMT interfaces
|
|
network_interfaces = []
|
|
network_interfaces.insert(0, nodes['nodes']['controller-0']['oam_if'])
|
|
network_interfaces.insert(1, nodes['nodes']['controller-0']['mgmt_if'])
|
|
configuration_file = os.path.join(
|
|
PROJECT_PATH, 'Config', 'config.ini')
|
|
configuration_type = config.get('general', 'CONFIGURATION_TYPE')
|
|
network.update_networks_config(
|
|
network_interfaces, configuration_file, configuration_type)
|
|
|
|
# Installing STX on main controller
|
|
controller_0 = nodes['nodes']['controller-0']
|
|
master_controller = Node(controller_0)
|
|
master_controller.boot_server_to_pxe()
|
|
master_controller.follow_node_installation()
|
|
|
|
return master_controller
|
|
|
|
|
|
def get_controller0_ip():
|
|
"""Returns master controller IP"""
|
|
|
|
nodes_file = os.path.join(THIS_PATH, '..', 'BareMetal',
|
|
'installation_setup.yaml')
|
|
nodes = yaml.load(open(nodes_file, encoding='utf8'))
|
|
controller_0 = nodes['controller-0']
|
|
master_controller = Node(controller_0)
|
|
|
|
return master_controller.installation_ip
|
|
|
|
|
|
def config_controller(config_file):
|
|
"""Configures master controller using its corresponding init file"""
|
|
|
|
config_controller_timeout = int(config.get(
|
|
'iso_installer', 'CONFIG_CONTROLLER_TIMEOUT'))
|
|
nodes_file = os.path.join(os.environ['PYTHONPATH'], 'baremetal',
|
|
'baremetal_setup.yaml')
|
|
nodes = yaml.safe_load(open(nodes_file, encoding='utf8'))
|
|
controller_0 = nodes['nodes']['controller-0']
|
|
master_controller = Node(controller_0)
|
|
serial_cmd = ('ipmitool -I lanplus -H {node_bmc_ip} -U {node_bmc_user} '
|
|
'-P {node_bmc_pswd} sol activate'
|
|
.format(node_bmc_ip=master_controller.bmc_ip,
|
|
node_bmc_user=master_controller.bmc_user,
|
|
node_bmc_pswd=master_controller.bmc_pswd))
|
|
|
|
configuring_controller = pexpect.spawn(serial_cmd)
|
|
configuring_controller.logfile = open('{}/iso_setup_installation.txt'
|
|
.format(LOG_PATH), 'wb')
|
|
configuring_controller.sendline('\r')
|
|
configuring_controller.expect(PROMPT)
|
|
LOG.info('Applying configuration (this will take several minutes)')
|
|
configuring_controller.sendline(
|
|
'sudo config_controller --force --config-file {}'.format(config_file))
|
|
configuring_controller.timeout = config_controller_timeout
|
|
configuring_controller.expect('Configuration was applied')
|
|
LOG.info(configuring_controller.before)
|
|
configuring_controller.logfile.close()
|
|
LOG.info('Closing the log')
|
|
configuring_controller.close()
|
|
closing_serial_connection = (
|
|
bash('ipmitool -I lanplus -H {node_bmc_ip} -U {node_bmc_user} '
|
|
'-P {node_bmc_pswd} sol deactivate'
|
|
.format(node_bmc_ip=master_controller.bmc_ip,
|
|
node_bmc_user=master_controller.bmc_user,
|
|
node_bmc_pswd=master_controller.bmc_pswd)))
|
|
if closing_serial_connection.stderr:
|
|
LOG.info(closing_serial_connection.stderr)
|
|
|
|
|
|
def install_secondary_nodes():
|
|
"""Installs STX on controller-1 and computes"""
|
|
|
|
nodes_file = os.path.join(THIS_PATH, '..', 'BareMetal',
|
|
'installation_setup.yml')
|
|
nodes = yaml.load(open(nodes_file, encoding='utf8'))
|
|
|
|
# Removing controller-0 from Nodes
|
|
controller_0 = nodes.pop('controller-0')
|
|
master_controller = Node(controller_0)
|
|
serial_cmd = ('ipmitool -I lanplus -H {node_bmc_ip} -U {node_bmc_user} '
|
|
'-P {node_bmc_pswd} sol activate'
|
|
.format(node_bmc_ip=master_controller.bmc_ip,
|
|
node_bmc_user=master_controller.bmc_user,
|
|
node_bmc_pswd=master_controller.bmc_pswd))
|
|
controller_0_serial = pexpect.spawn(serial_cmd)
|
|
|
|
# Loading openrc
|
|
controller_0_serial.sendline('source /etc/nova/openrc')
|
|
|
|
# Adding nodes to master controller
|
|
nodes_instances = list()
|
|
node_names = nodes.keys()
|
|
|
|
for node_name in node_names:
|
|
node = Node(nodes[node_name])
|
|
nodes_instances.append(node)
|
|
controller_0_serial.sendline('system host-add -n {name} '
|
|
'-p {personality} -m {mac_address}'
|
|
.format(name=node.name,
|
|
personality=node.personality,
|
|
mac_address=node.pxe_nic_mac))
|
|
node.boot_server_to_pxe()
|
|
|
|
node_installation_threads = list()
|
|
|
|
for nodes_instance in nodes_instances:
|
|
thread = threading.Thread(
|
|
target=nodes_instance.follow_node_installation())
|
|
LOG.info('Starting installation on %s', nodes_instance.name)
|
|
thread.start()
|
|
node_installation_threads.append(thread)
|
|
|
|
# Waiting for nodes to be installed
|
|
LOG.info('Waiting for nodes to be installed')
|
|
|
|
for node_installation_thread in node_installation_threads:
|
|
node_installation_thread.join()
|
|
|
|
LOG.info('All nodes have been installed successfully!')
|