###
# Copyright 2019 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 -*-
""" Certificates Command for rdmc """

import re
import copy
import time
import json

from collections import OrderedDict
from argparse import ArgumentParser, RawDescriptionHelpFormatter

import redfish.ris
from redfish.ris.ris import RisInstanceNotFoundError
from redfish.ris.utils import iterateandclear, diffdict, json_traversal, json_traversal_delete_empty
from redfish.ris.rmc_helper import IloResponseError, IdTokenError, InstanceNotFoundError, \
    InvalidPathError, InvalidSelectionError

from six.moves import input
from redfish.ris.utils import diffdict, navigatejson
from ilorest.rdmc_helper import ReturnCodes, RdmcError, InvalidCommandLineError, UI, \
    NoCurrentSessionEstablished, InvalidCommandLineErrorOPTS, InvalidFileInputError, \
    Encryption, NoChangesFoundOrMadeError, InvalidPropertyError, NoDifferencesFoundError

__eth_file__ = 'eth.json'
__subparsers__ = ['save', 'load']


class EthernetCommand():
    """ Commands for Ethernet Management Controller and RDE supported NIC card \
        configuration on server """

    def __init__(self):
        self.ident = {
            'name': 'ethernet',
            'usage': None,
            'description': 'Save or load from a JSON formatted file containing '
                           'properties pertaining to a system\'s iLO ethernet management controller. '
                           'Additionally enable/disable individual management controller NICs (including VNIC), '
                           'IPv4 and IPv6 addressing configuration and domain name servers.'
                           '\n\tBy default the JSON file will be named \'eth.json\'.\n\t'
                           '\n\t***Credentials for an iLO administrator level account must be provided as'
                           ' well as the iLO base URL.***'
                           '\n\n\tSave ethernet management controller data.\n\texample: ethernet save '
                           '\n\n\tLoad ethernet management controller data.\n\texample: ethernet load'
                           '\n\n\tSave ethernet management controller data to a different file, silently\n\t'
                           'ethernet save -f <filename> --silent.'
                           '\n\n\tLoad etherent management controller data from a different file, silently\n\t'
                           'ethernet load -f <filename> --silent.'
                           '\n\n\tEnable network interfaces by listing each interface to be enabled. **Note**:'
                           ' Non-existent interfaces will be omitted from configuration.\n\t '
                           'ethernet --enable_nic 1,2,3'
                           '\n\n\tDisable network interfaces by listing each interface to be disabled. **Note**:'
                           ' Non-existent interfaces will be omitted from configuration.\n\t '
                           'ethernet --disable_nic 1,2,3'
                           '\n\n\tConfigure Domain Name Servers (DNS) in a list: <DNS1> <DNS2>\n\t'
                           'ethernet --nameservers 8.8.8.8,1.1.1.1 OR ethernet --nameservers '
                           'dns_resolver1.aws.com, dns_resolver2.aws.com'
                           '\n\n\tConfigure Static IPv4 Settings. Provide a list of network settings\n\t'
                           'ethernet --network_ipv4 <ipv4 address>, <ipv4 gateway>, <ipv4 network mask>.'
                           '\n\n\tConfigure Proxy Settings. Provide a proxy server and port\n\t'
                           'ethernet --proxy http://proxy.company.net:8080'
                           '\n\n\tClear Proxy Settings. \n\t'
                           'ethernet --proxy None'
                           '\n\n\tConfigure Static IPv6 Settings. Provide a list of network settings\n\t'
                           'ethernet --network_ipv6 <ipv6 address>, <ipv6 gateway>, <ipv6 network mask>.',
            'summary': "Command for configuring Ethernet Management Controller Interfaces and "
                       "associated properties",
            'aliases': [],
            'auxcommands': []
        }
        self.cmdbase = None
        self.rdmc = None
        self.auxcommands = dict()
        self.save = None
        self.load = None
        self.eth_file = None  # set in validation
        self.eth_args = None
        self.saved_inputline = None  # set in validation

    def run(self, line, help_disp=False):
        """ Main Ethernet Management Controller Interfaces Run
        :param line: string of arguments passed in
        :type line: str.
        """
        if help_disp:
            self.parser.print_help()
            return ReturnCodes.SUCCESS
        try:
            ident_subparser = False
            for cmnd in __subparsers__:
                if cmnd in line:
                    (options, args) = self.rdmc.rdmc_parse_arglist(self, line)
                    ident_subparser = True
                    break
            if not ident_subparser:
                (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True)
        except:
            if ("-h" in line) or ("--help" in line):
                # self.rdmc.ui.printer(self.ident['usage'])
                return ReturnCodes.SUCCESS
            else:
                raise InvalidCommandLineErrorOPTS("")

        self.ethernetvalidation(options)

        if 'default' in options.command.lower():
            data = self.get_data(options)
            get_data = True
            for inst in data:
                if 'ethernetinterface' in inst.lower() or 'ethernetnetworkinterface' in inst.lower():
                    for path in data[inst]:
                        if 'managers/1/ethernetinterfaces/1' in path.lower() or 'managers/1/ethernetnetworkinterfaces/1' in path.lower():  # only process managers interfaces
                            if options.enable_nic or options.disable_nic:
                                get_data = False
                                self.enable_disable_nics(data[inst][path], options)

                            if (options.network_ipv4 or options.network_ipv6 or
                                options.nameservers or options.proxy) and 'virtual' not in \
                                    data[inst][path]['Name'].lower():
                                get_data = False
                                self.configure_static_settings(data[inst][path], options)
                                if options.proxy:
                                    return ReturnCodes.SUCCESS
            if get_data:
                self.output_data(data, options, get_data)
            else:
                self.load_main_function(options, data)

        if 'save' in options.command.lower():
            self.save = True
            self.load = False
            if 'save' in options.command.lower() and not options.ethfilename:
                self.rdmc.ui.printer('Saving configurations to default file \'%s\'.\n' % \
                                     self.eth_file)
            self.output_data(self.get_data(options), options, False)

        elif 'load' in options.command.lower():
            self.save = False
            self.load = True
            self.load_main_function(options)

        return ReturnCodes.SUCCESS

    def get_data(self, options):
        """Obtain data for Ethernet Management Interfaces, Ethernet System Interfaces, \
           Manager Network Services and iLO Date Time Types and subsequently save to a dictionary \
        :param options: command line options
        :type options: attribute
        """

        network_data_collection = eth_iface = manager_network_services = ilo_date_time = \
            OrderedDict()

        if self.rdmc.app.redfishinst.is_redfish:
            try:
                iface_results = self.rdmc.app.select(selector='ethernetinterface.')
            except RisInstanceNotFoundError:
                self.rdmc.ui.printer("Type \'EthernetInterface.\' not available.\n")
        else:
            try:
                iface_results = self.rdmc.app.select(selector='ethernetnetworkinterface.')
            except RisInstanceNotFoundError:
                self.rdmc.ui.printer("Type \'EthernetNetworkInterface.\' not available.\n")

        for inst in iface_results:
            try:
                eth_iface[inst.type].update({inst.path: inst.dict})
            except KeyError:
                eth_iface[inst.type] = {inst.path: inst.dict}
                continue
            except AttributeError:
                self.rdmc.ui.printer("Missing instance data for type \'%s\' : \'%s\'" % \
                                     (inst.type, inst.path))

        for inst in self.rdmc.app.select(selector='networkprotocol.'):
            try:
                manager_network_services[inst.type].update({inst.path: inst.dict})
            except KeyError:
                manager_network_services[inst.type] = {inst.path: inst.dict}
                continue
            except AttributeError:
                self.rdmc.ui.printer("Missing instance data for type \'%s\' : \'%s\'" % \
                                     (inst.type, inst.path))

        for inst in self.rdmc.app.select(selector='datetime.'):
            try:
                ilo_date_time[inst.type].update({inst.path: inst.dict})
            except KeyError:
                ilo_date_time[inst.type] = {inst.path: inst.dict}
                continue
            except AttributeError:
                self.rdmc.ui.printer("Missing instance data for type \'%s\' : \'%s\'" % \
                                     (inst.type, inst.path))

        network_data_collection.update(eth_iface)
        network_data_collection.update(manager_network_services)
        network_data_collection.update(ilo_date_time)

        return network_data_collection

    def output_data(self, data, options, get_only=False):
        """ Output data in requested json file or on console
        :param data: dictionary containing ethernet configuration data:
        :type data: dictionary
        :param options: command line options
        :type options: attribute
        :param get_only: parameter to only output to console (do not save data to file). \
        Essentially a targeted \'get\' command.
        :type: boolean
        """

        outdata = OrderedDict()

        outdata.update(self.rdmc.app.create_save_header())

        for _type in data:
            for path in data.get(_type):
                try:
                    outdata[_type.split('#')[-1]].update({path: self.rdmc.app.removereadonlyprops(data[_type][path])})
                except KeyError:
                    outdata[_type.split('#')[-1]] = {path: self.rdmc.app.removereadonlyprops(data[_type][path])}
                except Exception:
                    pass

        if self.eth_file and not get_only:
            if options.encryption:
                with open(self.eth_file, 'wb') as outfile:
                    outfile.write(Encryption().encrypt_file(json.dumps(outdata, indent=2,
                                                                       cls=redfish.ris.JSONEncoder),
                                                            options.encryption))
            else:
                with open(self.eth_file, 'w') as outfile:
                    outfile.write(json.dumps(outdata, indent=2, cls=redfish.ris.JSONEncoder))
        else:
            if options.json:
                self.rdmc.ui.print_out_json_ordered(outdata)
            else:
                UI().print_out_human_readable(outdata)

    def enable_disable_nics(self, data, options):

        if options.disable_nic:
            for ident in re.split('[, ]', options.disable_nic):
                try:
                    if int(data.get('Id')) == int(ident):
                        data.update({'InterfaceEnabled': False})
                        break
                except ValueError:
                    if data.get('Name') == ident:
                        data.update({'InterfaceEnabled': False})
                        break

        if options.enable_nic:
            for ident in re.split('[, ]', options.enable_nic):
                try:
                    if int(data.get('Id')) == int(ident):
                        data.update({'InterfaceEnabled': True})
                        break
                except ValueError:
                    if data.get('Name') == ident:
                        data.update({'InterfaceEnabled': True})
                        break

    def configure_static_settings(self, data, options):

        if options.network_ipv4:
            usr_data = re.split('[, ]', options.network_ipv4)
            if len(usr_data) > 2:
                data['IPv4Addresses'][0] = {'Address': usr_data[0],
                                            'Gateway': usr_data[1], 'SubnetMask': usr_data[2]}
                data['DHCPv4'].update({'DHCPEnabled': False})
                data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4'].update({'Enabled': False})
            else:
                raise InvalidCommandLineErrorOPTS("An invalid number of arguments provided to "
                                                  " quick networking configuration. Check \'--network_ipv4\' entry.")
        else:
            del data['IPv4Addresses']

        if options.network_ipv6:
            usr_data = re.split('[, ]', options.network_ipv6)
            if len(usr_data) > 2:
                next(iter(data['IPv6Addresses'])).update({'Address': \
                                                              usr_data[0], 'PrefixLength': usr_data[2]})
                data['IPv6DefaultGateway'] = options.network_ipv6[1]
                data['DHCPv4'].update({'DHCPEnabled': False})
                data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4'].update({'Enabled': False})
            else:
                raise InvalidCommandLineErrorOPTS("An invalid number of arguments provided to "
                                                  " quick networking configuration. Check \'--network_ipv4\' entry.")
        else:
            del data['IPv6Addresses']

        if options.nameservers:
            usr_data = re.split('[, ]', options.nameservers)
            if len(usr_data) <= 2:
                for elem in usr_data:
                    oem_ipv4 = data['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv4']
                    oem_ipv6 = data['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv6']
                    if "::" in elem:
                        tmp = oem_ipv6['DNSServers']
                        if elem not in tmp:
                            tmp.append(elem)
                            oem_ipv6['DNSServers'] = tmp
                    elif "." in elem:
                        tmp = oem_ipv4['DNSServers']
                        if elem not in tmp:
                            tmp.append(elem)
                            oem_ipv4['DNSServers'] = tmp
                    # tmp = data['NameServers']
                    # tmp.append(elem)
                    # data['NameServers'] = tmp
                    # tmp = data['StaticNameServers']
                    tmp = list()
                    tmp.append(elem)
                    data['StaticNameServers'] = tmp
            else:
                raise InvalidCommandLineErrorOPTS("Name Servers argument has to be less than or equal to 2")
        else:
            del data['StaticNameServers']
            del data['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv4']['DNSServers']
            del data['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv6']['DNSServers']

        if options.proxy != 'None':
            body = dict()
            body['Oem'] = {}
            body['Oem']['Hpe'] = {}
            body['Oem']['Hpe']['WebProxyConfiguration'] = {}
            proxy_body = body['Oem']['Hpe']['WebProxyConfiguration']
            proxy_body['ProxyServer'] = None
            proxy_body['ProxyUserName'] = None
            proxy_body['ProxyPassword'] = None
            if 'https' in options.proxy:
                proxy_body['ProxyPort'] = 443
            else:
                proxy_body['ProxyPort'] = 80
            if '@' in options.proxy:
                proxy = options.proxy.split('@')
                proxy_usr_pass = proxy[0]
                proxy_srv_port = proxy[1]
                if '//' in proxy_usr_pass:
                    proxy_usr_pass = proxy_usr_pass.split('//')[1]
                if ':' in proxy_srv_port:
                    proxy = proxy_srv_port.split(':')
                    proxy_body['ProxyServer'] = proxy[0]
                    proxy_body['ProxyPort'] = int(proxy[1])
                else:
                    proxy_body['ProxyServer'] = proxy_srv_port
                if ':' in proxy_usr_pass:
                    proxy = proxy_usr_pass.split(':')
                    proxy_body['ProxyPassword'] = proxy[1]
                    proxy_body['ProxyUserName'] = proxy[0]
                else:
                    proxy_body['ProxyUserName'] = proxy_usr_pass
            else:
                proxy_srv_port = options.proxy
                if '//' in proxy_srv_port:
                    proxy_srv_port = proxy_srv_port.split('//')[1]
                if ':' in proxy_srv_port:
                    proxy = proxy_srv_port.split(':')
                    proxy_body['ProxyServer'] = proxy[0]
                    proxy_body['ProxyPort'] = int(proxy[1])
                else:
                    proxy_body['ProxyServer'] = proxy_srv_port
            path = self.rdmc.app.getidbytype('NetworkProtocol.')

            if path and body:
                self.rdmc.ui.printer("Enabling Proxy configuration...\n", verbose_override=True)
                self.rdmc.app.patch_handler(path[0], body, service=False, silent=False)
        else:
            body = dict()
            body['Oem'] = {}
            body['Oem']['Hpe'] = {}
            body['Oem']['Hpe']['WebProxyConfiguration'] = {}
            proxy_body = body['Oem']['Hpe']['WebProxyConfiguration']
            proxy_body['ProxyServer'] = ""
            proxy_body['ProxyPort'] = None
            proxy_body['ProxyUserName'] = ""
            proxy_body['ProxyPassword'] = None
            path = self.rdmc.app.getidbytype('NetworkProtocol.')

            if path and body:
                self.rdmc.ui.printer("Clearing Proxy configuration...\n", verbose_override=True)
                self.rdmc.app.patch_handler(path[0], body, service=False, silent=False)

        json_traversal_delete_empty(data, None, None)

    def load_main_function(self, options, data=None):
        """
        Load main function. Handles SSO and SSL/TLS Certificates, kickoff load
        helper, load of patch file, get server status and issues system reboot
        and iLO reset after completion of all post and patch commands.
        :param options: command line options
        :type options: attribute
        :param data: data
        :type data: optional
        """

        if self.rdmc.app.redfishinst.is_redfish:
            eth_type = 'ethernetinterface.'
        else:
            eth_type = 'ethernetnetworkinterface.'

        linking_dict = {'manager_interfaces': eth_type, 'systems_interfaces': eth_type,
                        'manager_network_services': 'networkprotocol.', 'ilo_date_time':
                            'datetime.'}

        if not data:
            data = {}
            try:
                if options.encryption:
                    with open(self.eth_file, 'rb') as file_handle:
                        data = json.loads(Encryption().decrypt_file(file_handle.read(),
                                                                    options.encryption))
                else:
                    with open(self.eth_file, 'rb') as file_handle:
                        data = json.loads(file_handle.read())
            except:
                raise InvalidFileInputError("Invalid file formatting found. Verify the file has a "
                                            "valid JSON format.")

        for ilotype, subsect in data.items():
            _type = ilotype.split('.')[0]
            for _path in subsect:
                if not subsect[_path]:
                    continue
                elif 'ethernetinterface' in _type.lower() or \
                        'ethernetnetworkinterface' in _type.lower():
                    if 'managers' in _path.lower():
                        self.load_ethernet_aux(_type, _path, data[ilotype][_path])
                    elif 'systems' in _path.lower():
                        self.rdmc.ui.warn("Systems Ethernet Interfaces \'%s\' " \
                                          "cannot be modified." % _path)
                        continue
                elif 'datetime' in _type.lower():
                    if 'StaticNTPServers' in list(subsect.get(_path).keys()):
                        # must set NTP Servers to static in OEM then reset iLO for StaticNTPServers
                        # property to appear in iLODateTime
                        if self.rdmc.app.redfishinst.is_redfish:
                            eth_config_type = 'ethernetinterface'
                        else:
                            eth_config_type = 'ethernetnetworkinterface'
                        for key in list(data.keys()):
                            if key.split('.')[0].lower() == eth_config_type:
                                eth_config_type = key
                                for _path in data[eth_config_type]:
                                    if 'managers' in _path.lower():
                                        try:
                                            data[eth_config_type][_path]['DHCPv4'] \
                                                ['UseNTPServers'] = True
                                            data[eth_config_type][_path]['DHCPv6'] \
                                                ['UseNTPServers'] = True
                                            data[eth_config_type][_path]['Oem'] \
                                                [self.rdmc.app.typepath.defs.oemhp] \
                                                ['DHCPv4']['UseNTPServers'] = True
                                            data[eth_config_type][_path]['Oem'] \
                                                [self.rdmc.app.typepath.defs.oemhp] \
                                                ['DHCPv6']['UseNTPServers'] = True
                                            self.load_ethernet_aux(eth_config_type, _path,
                                                                   data[eth_config_type][_path])
                                        except KeyError:
                                            self.rdmc.ui.printer("Unable to configure " \
                                                                 "\'UseNTPServers\' for \'%s\'.\n" % _path)
                                        self.rdmc.ui.printer('iLO must be reset in order for '
                                                             'changes to static network time protocol servers to '
                                                             'take effect.\n')

    def load_ethernet_aux(self, _type, _path, ethernet_data):
        """ helper function for parsing and bucketting ethernet properties.
        :param _type: Redfish type used for querying the current server data
        :type _type: string
        :param _path: URI path to be patched.
        :type _path: string
        :param ethernet_data: JSON containing the ethernet instance (and valid, associated
                         properties) to be patched.
        :type ethernet_data: JSON

        """

        support_ipv6 = True
        dhcpv4curr = dhcpv4conf = oem_dhcpv4curr = oem_dhcpv4conf = dict()
        dhcpv6curr = dhcpv6conf = oem_dhcpv6curr = oem_dhcpv6conf = dict()
        errors = []

        ident_eth = False
        if 'EthernetInterface' in _type:
            for curr_sel in self.rdmc.app.select(_type.split('.')[0] + '.',
                                                 (self.rdmc.app.typepath.defs.hrefstring,
                                                  self.rdmc.app.typepath.defs.managerpath + '*'), path_refresh=True):
                if curr_sel.path == _path:
                    ident_eth = True
                    break
            # 'links/self/href' required when using iLO 4 (rest).
        elif 'EthernetNetworkInterface' in _type:
            for curr_sel in self.rdmc.app.select(_type.split('.')[0] + '.', ("links/self/" +
                                                                             self.rdmc.app.typepath.defs.hrefstring,
                                                                             self.rdmc.app.typepath.defs.managerpath + '*'),
                                                 path_refresh=True):
                if curr_sel.path == _path:
                    ident_eth = True
                    break
        else:
            raise Exception("Invalid type in management NIC load operation: \'%s\'" % _type)

        if not ident_eth:
            raise InvalidPathError("Path: '%s' is invalid/not identified on this server.\n" % \
                                   _path)

        ident_name = curr_sel.dict.get('Name')
        ident_id = curr_sel.dict.get('Id')
        # ENABLING ETHERNET INTERFACE SECTION
        try:
            # Enable the Interface if called for and not already enabled
            if ethernet_data.get('InterfaceEnabled') and not \
                    curr_sel.dict.get('InterfaceEnabled'):
                self.rdmc.app.patch_handler(_path, {"InterfaceEnabled": True}, silent=True)
                self.rdmc.ui.printer("NIC Interface Enabled.\n")
            # Disable the Interface if called for and not disabled already
            # No need to do anything else, just return
            elif not ethernet_data.get('InterfaceEnabled') and not \
                    curr_sel.dict.get('InterfaceEnabled'):
                self.rdmc.app.patch_handler(_path, {"InterfaceEnabled": False}, silent=True)
                self.rdmc.ui.warn("NIC Interface Disabled. All additional configurations "
                                  "omitted.")
                return
        except (KeyError, NameError, TypeError, AttributeError):
            # check OEM for NICEnabled instead
            if not curr_sel.dict['Oem'][self.rdmc.app.typepath.defs.oemhp]['NICEnabled'] and \
                    ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['NICEnabled']:
                self.rdmc.app.patch_handler(_path, {"Oem": {self.rdmc.app.typepath.defs.oemhp: \
                                                                {"NICEnabled": True}}}, silent=True)
                self.rdmc.ui.printer("NIC Interface Enabled.\n")
            elif not curr_sel.dict['Oem'][self.rdmc.app.typepath.defs.oemhp]['NICEnabled'] and not \
                    ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['NICEnabled']:
                self.rdmc.app.patch_handler(_path, {"Oem": {
                    self.rdmc.app.typepath.defs.oemhp: {"NICEnabled": False}}}, silent=True)
                self.rdmc.ui.printer("NIC Interface Disabled.\n")
                return
        # except IloResponseError should just be raised and captured by decorator. No point in
        # performing any other operations if the interface can not be set.

        # END ENABLING ETHERNET INTEFACE SECTION
        # ---------------------------------------
        # DETERMINE DHCPv4 and DHCPv6 States and associated flags

        if 'NICSupportsIPv6' in list(curr_sel.dict['Oem'][self.rdmc.app.typepath.defs.oemhp].keys()):
            support_ipv6 = curr_sel.dict['Oem'][self.rdmc.app.typepath.defs.oemhp]['NICSupportsIPv6']

        # obtain DHCPv4 Config and OEM
        try:
            if 'DHCPv4' in list(curr_sel.dict.keys()) and 'DHCPv4' in list(ethernet_data.keys()):
                dhcpv4curr = copy.deepcopy(curr_sel.dict['DHCPv4'])
                dhcpv4conf = copy.deepcopy(ethernet_data['DHCPv4'])
        except (KeyError, NameError, TypeError, AttributeError):
            errors.append("Unable to find Redfish DHCPv4 Settings.\n")
        finally:
            try:
                oem_dhcpv4curr = copy.deepcopy(curr_sel.dict['Oem']
                                               [self.rdmc.app.typepath.defs.oemhp]['DHCPv4'])
                oem_dhcpv4conf = copy.deepcopy(ethernet_data['Oem'] \
                                                   [self.rdmc.app.typepath.defs.oemhp]['DHCPv4'])
                ipv4curr = copy.deepcopy(curr_sel.dict['Oem'] \
                                             [self.rdmc.app.typepath.defs.oemhp]['IPv4'])
                ipv4conf = copy.deepcopy(ethernet_data['Oem'] \
                                             [self.rdmc.app.typepath.defs.oemhp]['IPv4'])
            except (KeyError, NameError):
                errors.append("Unable to find OEM Keys for DHCPv4 or IPv4")

        try:
            if support_ipv6:
                if 'DHCPv6' in list(curr_sel.dict.keys()) and 'DHCPv6' in list(ethernet_data.keys()):
                    dhcpv6curr = copy.deepcopy(curr_sel.dict['DHCPv6'])
                    dhcpv6conf = copy.deepcopy(ethernet_data['DHCPv6'])
            else:
                self.rdmc.ui.warn("NIC Does not support IPv6.")
        except (KeyError, NameError, TypeError, AttributeError):
            errors.append("Unable to find Redfish DHCPv6 Settings.\n")
        finally:
            try:
                oem_dhcpv4curr = copy.deepcopy(curr_sel.dict['Oem'] \
                                                   [self.rdmc.app.typepath.defs.oemhp]['DHCPv6'])
                oem_dhcpv6conf = copy.deepcopy(ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp] \
                                                   ['DHCPv6'])
                ipv6curr = copy.deepcopy(curr_sel.dict['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv6'])
                ipv6conf = copy.deepcopy(ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv6'])
            except (KeyError, NameError):
                errors.append("Unable to find OEM Keys for DHCPv6 or IPv6")

        try:
            # if DHCP Enable request but not currently enabled
            if dhcpv4conf.get('DHCPEnabled') and not curr_sel.dict['DHCPv4']['DHCPEnabled']:
                self.rdmc.app.patch_handler(_path, {"DHCPv4": {"DHCPEnabled": True}}, silent=True)
                self.rdmc.ui.printer("DHCP Enabled.\n")
            # if DHCP Disable request but currently enabled
            elif not dhcpv4conf['DHCPEnabled'] and curr_sel.dict['DHCPv4']['DHCPEnabled']:
                self.rdmc.app.patch_handler(_path, {"DHCPv4": {"DHCPEnabled": False}}, silent=True)
                dhcpv4conf['UseDNSServers'] = False
                dhcpv4conf['UseNTPServers'] = False
                dhcpv4conf['UseGateway'] = False
                dhcpv4conf['UseDomainName'] = False
                self.rdmc.ui.printer("DHCP Disabled.\n")
        except (KeyError, NameError, TypeError, AttributeError):
            # try with OEM
            try:
                if oem_dhcpv4conf.get('Enabled') and not curr_sel.dict \
                        ['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4']['Enabled']:
                    self.rdmc.app.patch_handler(_path, {'Oem': {
                        self.rdmc.app.typepath.defs.oemhp: {"DHCPv4": {"DHCPEnabled": True}}}},
                                                silent=True)
                    self.rdmc.ui.printer("DHCP Enabled.\n")
                    if 'IPv4Addresses' in ethernet_data:
                        del ethernet_data['IPv4Addresses']
                elif not oem_dhcpv4conf.get('Enabled') and curr_sel.dict \
                        ['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4']['Enabled']:
                    oem_dhcpv4conf['UseDNSServers'] = False
                    oem_dhcpv4conf['UseNTPServers'] = False
                    oem_dhcpv4conf['UseGateway'] = False
                    oem_dhcpv4conf['UseDomainName'] = False
                    self.rdmc.ui.printer("DHCP Disabled.\n")
            except (KeyError, NameError) as exp:
                errors.append("Failure in parsing or removing data in OEM DHCPv4: %s.\n" % exp)

        try:
            # if the ClientIDType is custom and we are missing the ClientID then this property can
            # not be set.
            if 'ClientIdType' in list(dhcpv4conf.keys()):
                if dhcpv4conf['ClientIdType'] == 'Custom' and 'ClientID' not in list(dhcpv4conf.keys()):
                    del ethernet_data['DHCPv4']['ClientIdType']
            elif 'ClientIdType' in list(oem_dhcpv4conf.keys()):
                if oem_dhcpv4conf['ClientIdType'] == 'Custom' and 'ClientID' not in \
                        list(oem_dhcpv4conf.keys()):
                    del ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4'] \
                        ['ClientIdType']
        except (KeyError, NameError, TypeError, AttributeError):
            try:
                if 'ClientIdType' in list(oem_dhcpv4conf.keys()):
                    if oem_dhcpv4conf['ClientIdType'] == 'Custom' and 'ClientID' not in \
                            list(oem_dhcpv4conf.keys()):
                        del ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DHCPv4'] \
                            ['ClientIdType']
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        # special considerations go here for things that need to stay despite diffdict
        # EX: IPv4 addresses (aka bug). Changing only one property within the
        # IPv4StaticAddresses or IPv4Addresses causes an issue during load. Must include IP,
        # subnet mask and gateway (they can not be patched individually).
        # spec_dict = {'Oem': {self.rdmc.app.typepath.defs.oemhp: {}}}
        spec_dict = dict()
        if "IPv4Addresses" in ethernet_data:
            spec_dict['IPv4Addresses'] = copy.deepcopy(ethernet_data['IPv4Addresses'])
        try:
            if "IPv4Addresses" in ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]:
                spec_dict['Oem'][self.rdmc.app.typepath.defs.oemhp]['IPv4Addresses'] = \
                    copy.deepcopy(ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp] \
                                      ['IPv4StaticAddresses'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass

        # diff and overwrite the original payload
        ethernet_data = diffdict(ethernet_data, curr_sel.dict)
        ethernet_data.update(spec_dict)

        # verify dependencies on those flags which are to be applied are eliminated
        try:
            # delete Domain name and FQDN if UseDomainName for DHCPv4 or DHCPv6
            # is present. can wait to apply at the end
            if dhcpv4conf.get('UseDomainName'):  # or dhcpv6conf['UseDomainName']:
                if 'DomainName' in ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]:
                    del ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DomainName']
                if 'FQDN' in ethernet_data:
                    del ethernet_data['FQDN']
        except (KeyError, NameError, TypeError, AttributeError):
            # try again with OEM
            try:
                if oem_dhcpv4conf.get('UseDomainName') or oem_dhcpv6conf.get('UseDomainName'):
                    if 'DomainName' in ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]:
                        del ethernet_data['Oem'][self.rdmc.app.typepath.defs.oemhp]['DomainName']
                    if 'FQDN' in ethernet_data:
                        del ethernet_data['FQDN']
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        try:
            # delete DHCP4 DNSServers from IPV4 dict if UseDNSServers Enabled
            # can wait to apply at the end
            if dhcpv4conf.get('UseDNSServers'):  # and ethernet_data.get('NameServers'):
                json_traversal_delete_empty(data=ethernet_data, remove_list=['NameServers'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass
        finally:
            try:
                if oem_dhcpv4conf.get('UseDNSServers'):
                    # del_sections('DNSServers', ethernet_data)
                    json_traversal_delete_empty(data=ethernet_data,
                                                remove_list=['DNSServers'])
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)
        try:
            if dhcpv4conf.get('UseWINSServers'):
                json_traversal_delete_empty(data=ethernet_data, remove_list=['WINServers'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass
        finally:
            try:
                if oem_dhcpv4conf.get('UseWINSServers'):
                    json_traversal_delete_empty(data=ethernet_data,
                                                remove_list=['WINServers', 'WINSRegistration'])
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        try:
            if dhcpv4conf.get('UseStaticRoutes'):
                json_traversal_delete_empty(data=ethernet_data, remove_list=['StaticRoutes'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass
        finally:
            try:
                if oem_dhcpv4conf.get('UseStaticRoutes'):
                    json_traversal_delete_empty(data=ethernet_data,
                                                remove_list=['StaticRoutes'])
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        try:
            # if using DHCPv4, remove static addresses
            if dhcpv4conf.get('DHCPEnabled'):
                json_traversal_delete_empty(data=ethernet_data,
                                            remove_list=['IPv4Addresses', 'IPv4StaticAddresses'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass
        finally:
            try:
                if oem_dhcpv4conf.get('Enabled'):
                    json_traversal_delete_empty(data=ethernet_data,
                                                remove_list=['IPv4Addresses', 'IPv4StaticAddresses'])
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        try:
            # if not using DHCPv6, remove static addresses from payload
            if dhcpv6conf.get('OperatingMode') == 'Disabled':
                json_traversal_delete_empty(data=ethernet_data,
                                            remove_list=['IPv6Addresses', 'IPv6StaticAddresses'])
        except (KeyError, NameError, TypeError, AttributeError):
            pass
        finally:
            try:
                if not oem_dhcpv6conf.get('StatefulModeEnabled'):
                    json_traversal_delete_empty(data=ethernet_data,
                                                remove_list=['IPv6Addresses', 'IPv6StaticAddresses'])
            except (KeyError, NameError) as exp:
                errors.append("Unable to remove property %s.\n" % exp)

        flags = dict()
        if dhcpv4conf:
            flags['DHCPv4'] = dhcpv4conf
        if dhcpv6conf:
            flags['DHCPv6'] = dhcpv6conf
        if oem_dhcpv4conf:
            flags['Oem'] = {self.rdmc.app.typepath.defs.oemhp: {'DHCPv4': oem_dhcpv4conf}}
        if oem_dhcpv6conf:
            flags['Oem'] = {self.rdmc.app.typepath.defs.oemhp: {'DHCPv6': oem_dhcpv6conf}}

        # verify dependencies on those flags which are to be applied are eliminated

        try:
            self.rdmc.app.patch_handler(_path, flags, silent=True)
        except IloResponseError as excp:
            errors.append("iLO Responded with the following errors setting DHCP: %s.\n" % excp)

        try:
            if 'AutoNeg' not in list(ethernet_data.keys()):
                json_traversal_delete_empty(data=ethernet_data,
                                            remove_list=['FullDuplex', 'SpeedMbps'])

            # if Full Duplex exists, check if FullDuplexing enabled. If so,
            # remove Speed setting.
            elif 'FullDuplex' in list(ethernet_data.keys()):
                json_traversal_delete_empty(data=ethernet_data,
                                            remove_list=['FullDuplex', 'SpeedMbps'])
        except (KeyError, NameError) as exp:
            errors.append("Unable to remove property %s.\n" % exp)

        try:
            if 'FrameSize' in list(ethernet_data.keys()):
                json_traversal_delete_empty(data=ethernet_data, remove_list=['FrameSize'])
        except (KeyError, NameError) as exp:
            errors.append("Unable to remove property %s.\n" % exp)

        self.patch_eth(_path, ethernet_data, errors)

        if errors and 'Virtual' not in ident_name:
            raise RdmcError("Ethernet configuration errors were found collectively on adapter: " \
                            "'%s, %s'\ntype: %s\nerrors: %s" % (ident_name, ident_id, _type, errors))

    def patch_eth(self, _path, eth_data, errors=[]):
        """ helper function for patching ethernet properties. Retry functionality with the ability
            to remove the offending property.
        :param _path: URI path to be patched.
        :type _path: string
        :param eth_data: JSON containing the ethernet instance (and valid, associated
                         properties) to be patched.
        :type eth_data: JSON
        :param errors: list of errors catalogued between attempts
        :type errors: list

        """

        try:
            if eth_data:
                # eth_data = json.dumps(eth_data)
                # import ast
                # eth_data = ast.literal_eval(eth_data)
                tmp = self.rdmc.app.patch_handler(_path, eth_data, silent=False, service=False)
                if tmp.status == 400:
                    raise InvalidPropertyError(tmp.dict['error'][next(iter(tmp.dict['error']))])
            else:
                raise NoDifferencesFoundError("No differences between existing iLO ethernet " \
                                              "configuration and new ethernet configuration.\nPath: %s\n" % _path)

        except InvalidPropertyError as excp:
            errors.append("iLO Responded with the following error: %s.\n" % excp)

            def drill_to_data(data, list_o_keys):
                if len(list_o_keys) > 1:
                    k = list_o_keys.pop(0)
                else:
                    del data[k]
                if isinstance(data, dict):
                    drill_to_data(data[k], list_o_keys)

            if hasattr(excp, 'message'):
                for key in excp.message[0]['MessageArgs']:
                    try:
                        eth_data.pop(key)
                    except (AttributeError, KeyError, StopIteration):
                        try:
                            drill_to_data(eth_data, list_o_keys=json_traversal(eth_data, key,
                                                                               ret_key_path=True))
                        except:
                            errors.append("Unable to find \'%s\'" % key)
                        return
                self.patch_eth(_path, eth_data)

        except NoDifferencesFoundError as excp:
            errors.append("%s" % excp)

    def ethernetvalidation(self, options):
        """ ethernet validation function
        :param options: command line options
        :type options: list.
        """

        self.cmdbase.login_select_validation(self, options)

        if options.ethfilename:
            if len(options.ethfilename) < 2:
                self.eth_file = options.ethfilename[0]
            else:
                raise InvalidCommandLineError("Only a single ethernet file may be specified.")
        else:
            self.eth_file = __eth_file__

    @staticmethod
    def options_argument_group(parser):
        """ Define option arguments group
        :param parser: The parser to add the login option group to
        :type parser: ArgumentParser/OptionParser
        """
        group = parser.add_argument_group()

        parser.add_argument(
            '--encryption',
            dest='encryption',
            help="Optionally include this flag to encrypt/decrypt a file"
                 " using the key provided.",
            default=None
        )
        parser.add_argument(
            '-f',
            '--ethfile',
            dest='ethfilename',
            help="""Optionally specify a JSON file to store or load ethernet configuration data.""",
            action="append",
            default=None,
        )

    def definearguments(self, customparser):
        """ Wrapper function for certificates command main function
        :param customparser: command line input
        :type customparser: parser.
        """
        if not customparser:
            return

        self.cmdbase.add_login_arguments_group(customparser)
        self.options_argument_group(customparser)
        subcommand_parser = customparser.add_subparsers(dest='command')
        default_parser = subcommand_parser.add_parser(
            'default',
            help="Obtain iLO management networking interface details and configure basic "
            "properties such as enablement/disablement, domain name servers, ipv4 and ipv6 "
            "networking configuration.",
            formatter_class=RawDescriptionHelpFormatter
        )
        default_parser.add_argument(
            '--enable_nic',
            dest='enable_nic',
            help="Enable network interfaces. List each interface to be enabled by ID: "
            "ethernet --enable_nic 1,2,3. **Note**: Non-existent interfaces will be omitted from "
            "configuration.",
            type=str,
            default=None,
        )
        default_parser.add_argument(
            '--disable_nic',
            dest='disable_nic',
            help="Disable network interfaces. List each interface to be disabled by ID: "
            "ethernet --disable_nic 1,2,3. **Note**: Non-existent interfaces will be omitted from "
            "configuration.",
            type=str,
            default=None,
        )
        default_parser.add_argument(
            '--nameservers',
            dest='nameservers',
            help="Configure physical and shared management network interface domain name "
            "servers (DNS) in a list as follows: <DNS1> <DNS2> "
            "Ex: ethernet --nameservers 8.8.8.8,1.1.1.1, ethernet --nameservers dns_resolver1.aws.com, "
            "dns_resolver2.aws.com",
            default=None,
        )
        default_parser.add_argument(
            '--proxy',
            dest='proxy',
            type=str,
            help="Configure or clear proxy server for the network "
            "Ex:\nTo set proxy\nethernet --proxy http://proxy.abc.net:8080 or \n"
            "To clear proxy settings\n"
            "ethernet --proxy None",
            default=None,
        )
        default_parser.add_argument(
            '--network_ipv4',
            dest='network_ipv4',
            help="Configure physical and shared management network interface static IPv4 "
            "settings. Settings provided in a list as follows: "
            "<ipv4 address>, <ipv4 gateway>, <ipv4 network mask>. Ex: ethernet --network_ipv4 "
            "192.168.1.10, 192.168.1.1, 255.255.0.0",
            default=None,
        )
        default_parser.add_argument(
            '--network_ipv6',
            dest='network_ipv6',
            help="Configure physical and shared management network interface static IPv6 "
            "settings. Settings provided in a list as follows: "
            "<ipv6 address>, <ipv6 gateway>, <ipv6 network mask>. Ex: ethernet --network_ipv6 "
            "0:0:0:0:ffff:c0a8:10e, 0:0:0:0:0:ffff:c0a8:101, 64. **Note**: IPv6 network mask"
            "is restricted to '64' bits.",
            default=None,
        )
        default_parser.add_argument(
            '-j',
            '--json',
            dest='json',
            action="store_true",
            help="Optionally include this flag if you wish to change the"
                 " displayed output to JSON format. Preserving the JSON data"
                 " structure makes the information easier to parse.",
            default=False
        )
        self.cmdbase.add_login_arguments_group(default_parser)
        save_help = 'Save a Network Configuration.'
        save_parser = subcommand_parser.add_parser(
            __subparsers__[0],
            help=save_help,
            description='{0}\n\texample: ethernet save\n\n\tSave iLO ethernet network management interface '
                        'settings to a non-default file name.\n\tEx: ethernet save -f networking.json\n\n\tSave '
                        'an encrypted iLO networking configuration file\n\texample: ethernet save --encryption '
                        '<ENCRYPTION_KEY>'.format(save_help),
            formatter_class=RawDescriptionHelpFormatter
        )
        self.cmdbase.add_login_arguments_group(save_parser)
        self.options_argument_group(save_parser)
        load_help = 'Load a Network Configuration.'
        load_parser = subcommand_parser.add_parser(
            __subparsers__[1],
            help=load_help,
            description='{0}\n\texample: ethernet load\n\n\tLoad iLO ethernet networking management interface '
                        'settings from a non-default file name.\n\tEx: ethernet load -f '
                        'networking.json\n\n\tLoad an encrypted iLO networking configuration file\n\texample: '
                        'ethernet load --encryption <ENCRYPTION KEY>'.format(load_help),
            formatter_class=RawDescriptionHelpFormatter
        )
        self.cmdbase.add_login_arguments_group(load_parser)
        self.options_argument_group(load_parser)
