Files

570 lines
20 KiB
Python

###
# 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 <iLO url/hostname> -u admin -p password\n\t--url"
" <iLO url/hostname> -u admin -p password\n\t--url <iLO url/"
"hostname> -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,
)