570 lines
20 KiB
Python
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,
|
|
)
|