### # Copyright 2016-2023 Hewlett Packard Enterprise, Inc. 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. ### # -*- coding: utf-8 -*- """Load Command for RDMC""" import json import os import shlex import subprocess import sys from datetime import datetime from six.moves import queue import redfish.ris from redfish.ris.rmc_helper import LoadSkipSettingError try: from rdmc_helper import ( Encryption, InvalidCommandLineError, InvalidCommandLineErrorOPTS, InvalidFileFormattingError, InvalidFileInputError, InvalidMSCfileInputError, MultipleServerConfigError, NoChangesFoundOrMadeError, ReturnCodes, ) except ImportError: from ilorest.rdmc_helper import ( Encryption, InvalidCommandLineError, InvalidCommandLineErrorOPTS, InvalidFileFormattingError, InvalidFileInputError, InvalidMSCfileInputError, MultipleServerConfigError, NoChangesFoundOrMadeError, ReturnCodes, ) try: from rdmc_helper import HARDCODEDLIST except: from ilorest.rdmc_helper import HARDCODEDLIST # default file name __filename__ = "ilorest.json" class LoadCommand: """Constructor""" def __init__(self): self.ident = { "name": "load", "usage": None, "description": "Run to load the default configuration" " file\n\texample: load\n\n\tLoad configuration file from a " "different file\n\tif any property values have changed, the " "changes are committed and the user is logged out of the server" "\n\n\texample: load -f output.json\n\n\tLoad configurations to " "multiple servers\n\texample: load -m mpfilename.txt -f output." "json\n\n\tNote: multiple server file format (1 server per new " "line)\n\t--url -u admin -p password\n\t--url" " -u admin -p password\n\t--url -u admin -p password", "summary": "Loads the server configuration settings from a file.", "aliases": [], "auxcommands": ["CommitCommand", "SelectCommand", "RawPatchCommand"], } self.filenames = None self.mpfilename = None self.queue = queue.Queue() self.cmdbase = None self.rdmc = None self.auxcommands = dict() def securebootremovereadonly(self, tmp): templist = [ "SecureBootCurrentBoot", "@odata.etag", "Id", "AutoNeg", "FQDN", "FullDuplex", "HostName", "IPv6Addresses", "IPv6DefaultGateway", "LinkStatus", "MaxIPv6StaticAddresses", "Name", "NameServers", "PermanentMACAddress", "ConfigurationSettings", "InterfaceType", "NICSupportsIPv6", "DomainName", "IPV6", "DNSServers", "MACAddress", "VLAN", "SpeedMbps", "DateTime", ] remove_data = self.rdmc.app.removereadonlyprops(tmp, False, True, templist) return remove_data def run(self, line, help_disp=False): """Main load worker function :param line: command line input :type line: string. """ if help_disp: self.parser.print_help() return ReturnCodes.SUCCESS try: (options, _) = self.rdmc.rdmc_parse_arglist(self, line) except (InvalidCommandLineErrorOPTS, SystemExit): if ("-h" in line) or ("--help" in line): return ReturnCodes.SUCCESS else: raise InvalidCommandLineErrorOPTS("") self.loadvalidation(options) returnvalue = False if options.mpfilename: self.rdmc.ui.printer("Loading configuration for multiple servers...\n") else: self.rdmc.ui.printer("Loading configuration...\n") for files in self.filenames: if not os.path.isfile(files): raise InvalidFileInputError( "File '%s' doesn't exist. Please create file by running save command." % files ) if options.encryption: with open(files, "rb") as myfile: data = myfile.read() data = Encryption().decrypt_file(data, options.encryption) loadcontents = json.loads(data) else: try: with open(files, "r") as myfile: loadcontents = json.load(myfile) except: raise InvalidFileFormattingError("Invalid file formatting found in file %s" % files) if options.mpfilename: mfile = options.mpfilename outputdir = None if options.outdirectory: outputdir = options.outdirectory if self.runmpfunc(mpfile=mfile, lfile=files, outputdir=outputdir): return ReturnCodes.SUCCESS else: raise MultipleServerConfigError("One or more servers failed to load given configuration.") results = False eth_conf = False validation_errs = [] for loadcontent in loadcontents: for content, loaddict in loadcontent.items(): inputlist = list() if content == "Comments": continue if "EthernetInterface" in content: if options.force_network_config: import platform import tempfile patchpath = "/redfish/v1/Managers/1/EthernetInterfaces/1/" try: payload = loadcontent[content][patchpath] except KeyError: continue payload = loadcontent[content][patchpath] remove_odata = self.securebootremovereadonly(payload) payload.update(remove_odata) if "StaticNameServers" in payload: payload["StaticNameServers"] = [ item for item in payload["StaticNameServers"] if item != "::" ] tempdir = "/tmp" if platform.system() == "Darwin" else tempfile.gettempdir() temp_file = os.path.join(tempdir, "temp_patch.json") out_file = open(temp_file, "w") patch_payload = {patchpath: payload} json.dump(patch_payload, out_file, indent=6) out_file.close() self.auxcommands["rawpatch"].run(temp_file + " --service") os.remove(temp_file) eth_conf = True continue else: self.rdmc.ui.printer( "Skipping network configurations as" " --force_network_config is not included.\n" ) continue inputlist.append(content) if options.biospassword: inputlist.extend(["--biospassword", options.biospassword]) self.auxcommands["select"].selectfunction(inputlist) if self.rdmc.app.selector.lower() not in content.lower(): raise InvalidCommandLineError("Selector not found.\n") try: for _, items in loaddict.items(): remove_odata = self.securebootremovereadonly(items) items.update(remove_odata) try: if self.rdmc.app.loadset( seldict=items, latestschema=options.latestschema, uniqueoverride=options.uniqueoverride, ): results = True except LoadSkipSettingError: returnvalue = True results = True except: raise except redfish.ris.ValidationError as excp: errs = excp.get_errors() validation_errs.append({self.rdmc.app.selector: errs}) except: raise try: if results: self.auxcommands["commit"].commitfunction(options=options) reboot_needed = False for n in inputlist: n = n.lower() if "bios" in n or "boot" in n or "iscsi" in n: reboot_needed = True if reboot_needed: self.rdmc.ui.printer("Reboot is required for settings to take effect.\n") except NoChangesFoundOrMadeError as excp: if returnvalue: pass else: raise excp if validation_errs: for validation_err in validation_errs: for err_type in validation_err: self.rdmc.ui.error("Validation error(s) in type %s:\n" % err_type) for err in validation_err[err_type]: if isinstance(err, redfish.ris.RegistryValidationError): self.rdmc.ui.error(err.message) try: if err.reg: self.rdmc.ui.error(str(err.sel)) except: pass raise redfish.ris.ValidationError() if not results: if eth_conf: continue else: self.rdmc.ui.printer("No differences found from current configuration.\n") # Return code if returnvalue: return ReturnCodes.LOAD_SKIP_SETTING_ERROR return ReturnCodes.SUCCESS def loadvalidation(self, options): """Load method validation function :param options: command line options :type options: list. """ if self.rdmc.opts.latestschema: options.latestschema = True try: self.cmdbase.login_validation(self, options) except Exception: if options.mpfilename: pass else: raise # filename validations and checks if options.filename: self.filenames = options.filename elif self.rdmc.config: if self.rdmc.config.defaultloadfilename: self.filenames = [self.rdmc.config.defaultloadfilename] if not self.filenames: self.filenames = [__filename__] def verify_file(self, filedata, inputfile): """Function used to handle oddly named files and convert to JSON :param filedata: input file data :type filedata: string. :param inputfile: current input file :type inputfile: string. """ try: tempholder = json.loads(filedata) return tempholder except: raise InvalidFileFormattingError("Invalid file formatting found in file %s" % inputfile) def get_current_selector(self, path=None): """Returns current selected content minus hard coded list :param path: current path :type path: string. """ contents = self.rdmc.app.monolith.path[path] if not contents: contents = list() for content in contents: for k in list(content.keys()): if k.lower() in HARDCODEDLIST or "@odata" in k.lower(): del content[k] return contents def runmpfunc(self, mpfile=None, lfile=None, outputdir=None): """Main worker function for multi file command :param mpfile: configuration file :type mpfile: string. :param lfile: custom file name :type lfile: string. :param outputdir: custom output directory :type outputdir: string. """ # self.logoutobj.run("") data = self.validatempfile(mpfile=mpfile, lfile=lfile) if not data: return False processes = [] finalreturncode = True outputform = "%Y-%m-%d-%H-%M-%S" if outputdir: if outputdir.endswith(('"', "'")) and outputdir.startswith(('"', "'")): outputdir = outputdir[1:-1] if not os.path.isdir(outputdir): self.rdmc.ui.error("The give output folder path does not exist.\n") raise InvalidCommandLineErrorOPTS("") dirpath = outputdir else: dirpath = os.getcwd() dirname = "%s_%s" % (datetime.now().strftime(outputform), "MSClogs") createdir = os.path.join(dirpath, dirname) os.mkdir(createdir) oofile = open(os.path.join(createdir, "CompleteOutputfile.txt"), "w+") self.rdmc.ui.printer("Create multiple processes to load configuration " "concurrently to all servers...\n") while True: if not self.queue.empty(): line = self.queue.get() else: break finput = "\n" + "Output for " + line[line.index("--url") + 1] + ": \n\n" urlvar = line[line.index("--url") + 1] if "python" in os.path.basename(sys.executable.lower()): # If we are running from source we have to add the python file to the command listargforsubprocess = [sys.executable, sys.argv[0]] + line else: listargforsubprocess = [sys.executable] + line if os.name != "nt": listargforsubprocess = " ".join(listargforsubprocess) urlfilename = urlvar.split("//")[-1] logfile = open(os.path.join(createdir, urlfilename + ".txt"), "w+") pinput = subprocess.Popen(listargforsubprocess, shell=True, stdout=logfile, stderr=logfile) processes.append((pinput, finput, urlfilename, logfile)) for pinput, finput, urlfilename, logfile in processes: pinput.wait() returncode = pinput.returncode finalreturncode = finalreturncode and not returncode logfile.close() logfile = open(os.path.join(createdir, urlfilename + ".txt"), "r+") oofile.write(finput + str(logfile.read())) oofile.write("-x+x-" * 16) logfile.close() if returncode == 0: self.rdmc.ui.printer("Loading Configuration for {} : SUCCESS\n".format(urlfilename)) else: self.rdmc.ui.error("Loading Configuration for {} : FAILED\n".format(urlfilename)) self.rdmc.ui.error( "ILOREST return code : {}.\nFor more details please check " "{}.txt under {} directory.\n".format(returncode, urlfilename, createdir) ) oofile.close() if finalreturncode: self.rdmc.ui.printer("All servers have been successfully configured.\n") return finalreturncode def validatempfile(self, mpfile=None, lfile=None): """Validate temporary file :param mpfile: configuration file :type mpfile: string. :param lfile: custom file name :type lfile: string. """ self.rdmc.ui.printer("Checking given server information...\n") if not mpfile: return False if not os.path.isfile(mpfile): raise InvalidFileInputError( "File '%s' doesn't exist, please " "create file by running save command." % mpfile ) try: with open(mpfile, "r") as myfile: data = list() cmdtorun = ["load"] cmdargs = ["-f", str(lfile)] globalargs = ["-v", "--nocache"] while True: line = myfile.readline() if not line: break if line.endswith(os.linesep): line.rstrip(os.linesep) args = shlex.split(line, posix=False) if len(args) < 5: self.rdmc.ui.error("Incomplete data in input file: {}\n".format(line)) raise InvalidMSCfileInputError("Please verify the " "contents of the %s file" % mpfile) else: linelist = globalargs + cmdtorun + args + cmdargs line = str(line).replace("\n", "") self.queue.put(linelist) data.append(linelist) except Exception as excp: raise excp if data: return data return False def definearguments(self, customparser): """Wrapper function for new command main function :param customparser: command line input :type customparser: parser. """ if not customparser: return self.cmdbase.add_login_arguments_group(customparser) customparser.add_argument( "-f", "--filename", dest="filename", help="Use this flag if you wish to use a different" " filename than the default one. The default filename is" " %s." % __filename__, action="append", default=None, ) customparser.add_argument( "-m", "--multiprocessing", dest="mpfilename", help="""use the provided filename to obtain data""", default=None, ) customparser.add_argument( "--outputdirectory", dest="outdirectory", help="""use the provided directory to output data for multiple server configuration""", default=None, ) customparser.add_argument( "--latestschema", dest="latestschema", action="store_true", help="Optionally use the latest schema instead of the one " "requested by the file. Note: May cause errors in some data " "retrieval due to difference in schema versions.", default=None, ) customparser.add_argument( "--uniqueoverride", dest="uniqueoverride", action="store_true", help="Override the measures stopping the tool from writing " "over items that are system unique.", default=False, ) customparser.add_argument( "--encryption", dest="encryption", help="Optionally include this flag to encrypt/decrypt a file " "using the key provided.", default=None, ) customparser.add_argument( "--reboot", dest="reboot", help="Use this flag to perform a reboot command function after" " completion of operations. For help with parameters and" " descriptions regarding the reboot flag, run help reboot.", default=None, ) customparser.add_argument( "--force_network_config", dest="force_network_config", help="Use this flag to force set network configuration." "Network settings will be skipped if the flag is not included.", action="store_true", default=None, )