X-Git-Url: https://gerrit.akraino.org/r/gitweb?p=ta%2Fremote-installer.git;a=blobdiff_plain;f=src%2Fremoteinstaller%2Finstaller%2Finstall.py;fp=src%2Fremoteinstaller%2Finstaller%2Finstall.py;h=1d600662ec4fb5c2421686bb607a409fc5a90556;hp=0000000000000000000000000000000000000000;hb=f9adb9143ef94b16ae16941652e75deccad506ef;hpb=3a2c5cc0fe9265242032882d68129b7faf47235c diff --git a/src/remoteinstaller/installer/install.py b/src/remoteinstaller/installer/install.py new file mode 100644 index 0000000..1d60066 --- /dev/null +++ b/src/remoteinstaller/installer/install.py @@ -0,0 +1,389 @@ +# Copyright 2019 Nokia + +# 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 logging +import sys +import argparse +import subprocess +import os +import importlib +import time + +from yaml import load +from netaddr import IPNetwork +from netaddr import IPAddress + +import hw_detector.hw_detect_lib as hw_detect +from hw_detector.hw_exception import HWException +from remoteinstaller.installer.bmc_management.bmctools import BMCException +from remoteinstaller.installer.catfile import CatFile +from remoteinstaller.installer.catfile import CatFileException + +class InstallException(Exception): + pass + +class Installer(object): + SSH_OPTS = '-o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=60' + + def __init__(self, args): + self._yaml_path = args.yaml + self._boot_iso_path = args.boot_iso + self._iso_url = args.iso + self._logdir = args.logdir + self._callback_url = args.callback_url + self._client_key = args.client_key + self._client_cert = args.client_cert + self._ca_cert = args.ca_cert + self._own_ip = args.host_ip + self._tag = args.tag + self._http_port = args.http_port + + # TODO + self._disable_bmc_initial_reset = True + self._disable_other_bmc_reset = True + + self._uc = self._read_user_config(self._yaml_path) + self._vip = None + self._first_controller_ip = None + self._first_controller_bmc = None + + @staticmethod + def _read_user_config(config_file_path): + logging.debug('Read user config from %s', config_file_path) + + try: + with open(config_file_path, 'r') as f: + y = load(f) + + return y + except Exception as ex: + raise InstallException(str(ex)) + + @staticmethod + def _execute_shell(command, desc=''): + logging.debug('Execute %s with command: %s', desc, command) + + p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + if p.returncode: + logging.warning('Failed to %s: %s (rc=%s)', desc, out, p.returncode) + raise InstallException('Failed to {}'.format(desc)) + + return (p.returncode, out) + + def _attach_iso_as_virtual_media(self, file_list): + logging.info('Attach ISO as virtual media') + + nfs_mount = os.path.dirname(self._boot_iso_path) + boot_iso_filename = os.path.basename(self._boot_iso_path) + patched_iso_filename = '{}/{}_{}'.format(nfs_mount, self._tag, boot_iso_filename) + + self._patch_iso(patched_iso_filename, file_list) + + self._first_controller_bmc.attach_virtual_cd(self._own_ip, nfs_mount, os.path.basename(patched_iso_filename)) + + def _create_cloud_config(self, first_controller): + logging.info('Create network config file') + + domain = self._uc['hosts'][first_controller].get('network_domain') + extnet = self._uc['networking']['infra_external']['network_domains'][domain] + + vlan = extnet.get('vlan') + first_ip = extnet['ip_range_start'] + gateway = extnet['gateway'] + dns = self._uc['networking']['dns'][0] + cidr = extnet['cidr'] + prefix = IPNetwork(cidr).prefixlen + + self._vip = str(IPAddress(first_ip)) + + pre_allocated_ips = self._uc['hosts'][first_controller].get('pre_allocated_ips', None) + if pre_allocated_ips: + pre_allocated_infra_external_ip = pre_allocated_ips.get('infra_external', None) + self._first_controller_ip = str(IPAddress(pre_allocated_infra_external_ip)) + + if not self._first_controller_ip: + self._first_controller_ip = str(IPAddress(first_ip)+1) + + controller_network_profile = self._uc['hosts'][first_controller]['network_profiles'][0] + mappings = self._uc['network_profiles'][controller_network_profile]['interface_net_mapping'] + for interface in mappings: + if 'infra_external' in mappings[interface]: + infra_external_interface = interface + break + + if infra_external_interface.startswith('bond'): + bonds = self._uc['network_profiles'][controller_network_profile]['bonding_interfaces'] + device = bonds[infra_external_interface][0] + else: + device = infra_external_interface + + # TODO + # ROOTFS_DISK + + logging.debug('VLAN=%s', vlan) + logging.debug('DEV=%s', device) + logging.debug('IP=%s/%s', self._first_controller_ip, prefix) + logging.debug('DGW=%s', gateway) + logging.debug('NAMESERVER=%s', dns) + logging.debug('ISO_URL="%s"', self._iso_url) + + network_config_filename = '{}/network_config'.format(os.getcwd()) + with open(network_config_filename, 'w') as f: + if vlan: + f.write('VLAN={}\n'.format(vlan)) + f.write('DEV={}\n'.format(device)) + f.write('IP={}/{}\n'.format(self._first_controller_ip, prefix)) + f.write('DGW={}\n'.format(gateway)) + f.write('NAMESERVER={}\n'.format(dns)) + f.write('\n') + f.write('ISO_URL="{}"'.format(self._iso_url)) + + logging.debug('CALLBACK_URL="%s"', self._callback_url) + + callback_url_filename = '{}/callback_url'.format(os.getcwd()) + with open(callback_url_filename, 'w') as f: + f.write(self._callback_url) + + if self._client_cert: + return [self._yaml_path, + network_config_filename, + callback_url_filename, + self._client_key, + self._client_cert, + self._ca_cert] + + return [self._yaml_path, + network_config_filename, + callback_url_filename] + + def _patch_iso(self, iso_target, file_list): + logging.info('Patch boot ISO') + logging.debug('Original ISO: %s', self._boot_iso_path) + logging.debug('Target ISO: %s', iso_target) + + file_list_str = ' '.join(file_list) + logging.debug('Files to add: %s', file_list_str) + + self._execute_shell('/usr/bin/patchiso.sh {} {} {}'.format(self._boot_iso_path, + iso_target, + file_list_str), 'patch ISO') + + def _put_file(self, ip, user, passwd, file_name, to_file=''): + self._execute_shell('sshpass -p {} scp {} {} {}@{}:{}'.format(passwd, + Installer.SSH_OPTS, + file_name, + user, + ip, + to_file)) + + def _get_file(self, log_dir, ip, user, passwd, file_name, recursive=False): + if recursive: + self._execute_shell('sshpass -p {} scp {} -r {}@{}:{} {}'.format(passwd, + Installer.SSH_OPTS, + user, + ip, + file_name, + log_dir)) + else: + self._execute_shell('sshpass -p {} scp {} {}@{}:{} {}'.format(passwd, + Installer.SSH_OPTS, + user, + ip, + file_name, + log_dir)) + + def _get_node_logs(self, log_dir, ip, user, passwd): + self._get_file(log_dir, ip, user, passwd, '/srv/deployment/log/cm.log') + self._get_file(log_dir, ip, user, passwd, '/srv/deployment/log/bootstrap.log') + self._get_file(log_dir, ip, user, passwd, '/var/log/ironic', recursive=True) + + def _get_journal_logs(self, log_dir, ip, user, passwd): + self._put_file(ip, user, passwd, '/opt/remoteinstaller/get_journals.sh') + self._put_file(ip, user, passwd, '/opt/remoteinstaller/print_hosts.py') + + self._execute_shell('sh ./get_journals.sh') + + self._get_file(log_dir, ip, user, passwd, '/tmp/node_journals.tgz') + + def _get_logs_from_console(self, log_dir, bmc, admin_user, admin_passwd): + bmc_host = bmc.get_host() + bmc_user = bmc.get_user() + bmc_passwd = bmc.get_passwd() + + log_file = '{}/cat_bootstrap.log'.format(log_dir) + try: + cat_file = CatFile(bmc_host, bmc_user, bmc_passwd, admin_user, admin_passwd) + cat_file.cat('/srv/deployment/log/bootstrap.log', log_file) + except CatFileException as ex: + logging.info('Could not cat file from console: %s', str(ex)) + + cat_file = CatFile(bmc_host, bmc_user, bmc_passwd, 'root', 'root') + cat_file.cat('/srv/deployment/log/bootstrap.log', log_file) + + def get_logs(self, log_dir, admin_passwd): + admin_user = self._uc['users']['admin_user_name'] + + ssh_command = 'nc -w1 {} 22 /dev/null'.format(self._first_controller_ip) + ssh_responds = self._execute_shell(ssh_command) + + if ssh_responds: + self._get_node_logs(log_dir, self._first_controller_ip, admin_user, admin_passwd) + + self._get_journal_logs(log_dir, self._first_controller_ip, admin_user, admin_passwd) + else: + self._get_logs_from_console(log_dir, + self._first_controller_bmc, + admin_user, + admin_passwd) + + def install(self): + try: + logging.info('Start install') + + if os.path.dirname(self._boot_iso_path) == '': + self._boot_iso_path = '{}/{}'.format(os.getcwd(), self._boot_iso_path) + + if self._logdir: + if not os.path.exists(self._logdir): + os.makedirs(self._logdir) + else: + self._logdir = '.' + + other_bmcs = [] + first_controller = None + for hw in sorted(self._uc['hosts']): + logging.info('HW node name is %s', hw) + + if not first_controller: + if 'controller' in self._uc['hosts'][hw]['service_profiles'] or \ + 'caas_master' in self._uc['hosts'][hw]['service_profiles']: + first_controller = hw + logging.info('HW is first controller') + + host = self._uc['hosts'][hw]['hwmgmt']['address'] + user = self._uc['hosts'][hw]['hwmgmt']['user'] + passwd = self._uc['hosts'][hw]['hwmgmt']['password'] + + bmc_log_path = '{}/{}.log'.format(self._logdir, hw) + + try: + hw_data = hw_detect.get_hw_data(host, user, passwd, False) + except HWException as e: + error = "Harware not detected for {}: {}".format(hw, str(e)) + logging.error(error) + raise BMCException(error) + + logging.info("Hardware belongs to %s product family", (hw_data['product_family'])) + if 'Unknown' in hw_data['product_family']: + error = "Hardware not detected for %s" % hw + logging.error(error) + raise BMCException(error) + + bmc_mod_name = 'remoteinstaller.installer.bmc_management.{}'.format(hw_data['product_family'].lower()) + bmc_mod = importlib.import_module(bmc_mod_name) + bmc_class = getattr(bmc_mod, hw_data['product_family']) + bmc = bmc_class(host, user, passwd, bmc_log_path) + bmc.set_host_name(hw) + + bmc.setup_sol() + + if hw != first_controller: + other_bmcs.append(bmc) + bmc.power('off') + else: + self._first_controller_bmc = bmc + + logging.debug('First controller: %s', first_controller) + + if not self._disable_bmc_initial_reset: + self._first_controller_bmc.reset() + time_after_reset = int(time.time()) + + if not self._disable_other_bmc_reset: + for bmc in other_bmcs: + bmc.reset() + + if not self._disable_bmc_initial_reset: + # Make sure we sleep at least 6min after the first controller BMC reset + sleep_time = 6*60-int(time.time())-time_after_reset + if sleep_time > 0: + logging.debug('Waiting for first controller BMC to stabilize \ + (%s sec) after reset', sleep_time) + time.sleep(sleep_time) + + config_file_names = self._create_cloud_config(first_controller) + + self._first_controller_bmc.setup_boot_options_for_virtual_media() + + self._attach_iso_as_virtual_media(config_file_names) + + self._first_controller_bmc.boot_from_virtual_media() + + self._first_controller_bmc.wait_for_bootup() + + self._first_controller_bmc.close() + + access_info = {'vip': self._vip, + 'installer_node_ip': self._first_controller_ip, + 'admin_user': self._uc['users']['admin_user_name']} + + return access_info + except BMCException as ex: + logging.error('Installation failed: %s', str(ex)) + raise InstallException(str(ex)) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-y', '--yaml', required=True, + help='User config yaml file path') + parser.add_argument('-b', '--boot-iso', required=True, + help='Path to boot ISO image in NFS mount') + parser.add_argument('-i', '--iso', required=True, + help='URL to ISO image') + parser.add_argument('-d', '--debug', action='store_true', required=False, + help='Debug level for logging') + parser.add_argument('-l', '--logdir', required=False, + help='Directory path for log files') + parser.add_argument('-c', '--callback-url', required=True, + help='Callback URL for progress reporting') + parser.add_argument('-K', '--client-key', required=True, + help='Client key file path') + parser.add_argument('-C', '--client-cert', required=True, + help='Client cert file path') + parser.add_argument('-A', '--ca-cert', required=True, + help='CA cert file path') + parser.add_argument('-H', '--host-ip', required=True, + help='IP for hosting HTTPD and NFS') + parser.add_argument('-T', '--http-port', required=False, + help='Port for HTTPD') + + parsed_args = parser.parse_args() + + if parsed_args.debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.basicConfig(stream=sys.stdout, level=log_level) + + logging.debug('args: %s', parsed_args) + installer = Installer(parsed_args) + + installer.install() + +if __name__ == "__main__": + sys.exit(main())