diff --git a/.gitignore b/.gitignore index 38e7f3d1..e67237db 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ develop-eggs .installed.cfg lib64 env +venv* # Documentation docs/_build diff --git a/.travis.yml b/.travis.yml index 2d139b43..46b811e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,10 @@ sudo: true python: - "2.7" - - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" addons: apt: diff --git a/README.md b/README.md index b6c820f3..4be17c77 100755 --- a/README.md +++ b/README.md @@ -117,6 +117,12 @@ jsnap2py -i test_interface.conf -o interface.yml ``` For more information please refer [jsnap2py-wiki] (https://github.com/Juniper/jsnapy/wiki/7.-jsnap2py) + +Logging: +-------- +Log related details will be extracted from "logging.yml" in jsnapy.cfg file. + Default path is "/etc/jsnapy/logging.yml" + CONTRIBUTORS ------------- diff --git a/development.txt b/development.txt index 957212ff..5982e6e1 100644 --- a/development.txt +++ b/development.txt @@ -1,6 +1,6 @@ -r requirements.txt -coverage==4.0.0 # http://nedbatchelder.com/code/coverage/ +coverage # http://nedbatchelder.com/code/coverage/ mock==1.0.1 # http://www.voidspace.org.uk/python/mock/ nose # http://nose.readthedocs.org/en/latest/ pep8 # https://github.com/jcrocholl/pep8 diff --git a/lib/jnpr/jsnapy/__init__.py b/lib/jnpr/jsnapy/__init__.py index d2a4ac51..0630c20b 100755 --- a/lib/jnpr/jsnapy/__init__.py +++ b/lib/jnpr/jsnapy/__init__.py @@ -19,13 +19,22 @@ class DirStore: custom_dir = None +# Function added by @gcasella +# To check if the user is currently running the installation inside of a virtual environment that was installed using the `python3 -m venv venv` command. +def venv_check(): + + if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): + return True + else: + return False def get_config_location(file='jsnapy.cfg'): p_locations = [] if 'JSNAPY_HOME' in os.environ: p_locations = [os.environ['JSNAPY_HOME']] - if hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 22-29. + if venv_check() is True: p_locations.extend([os.path.join(os.path.expanduser("~"), '.jsnapy'), os.path.join(sys.prefix, 'etc', 'jsnapy')]) elif 'win32' in sys.platform: diff --git a/lib/jnpr/jsnapy/check.py b/lib/jnpr/jsnapy/check.py index 399a58aa..c174f99a 100755 --- a/lib/jnpr/jsnapy/check.py +++ b/lib/jnpr/jsnapy/check.py @@ -27,10 +27,15 @@ class Comparator: - def __init__(self): + def __init__(self, **kwargs): + """ + Comparator object constructor. + :param int port. + + """ self.logger_check = logging.getLogger(__name__) self.log_detail = {'hostname': None} - + self.port = kwargs.get('port', None) def is_op(self, op): """ @@ -63,6 +68,8 @@ def generate_snap_file(self, device, prefix, name, reply_format): """ This function generates name of snapshot files """ + if self.port is not None: + device = "{}_{}".format(device, self.port) if os.path.isfile(prefix): return prefix else: @@ -209,7 +216,7 @@ def expression_evaluator(self, elem_test, op, x_path, id_list, iter, teston, ele_list = ele_list + [elements.strip() for elements in xml_paras.split(',') if - elements.strip() is not ''] + elements.strip() != ''] else: ele_list = [elements.strip() for elements in ele.split(',')] @@ -226,7 +233,7 @@ def expression_evaluator(self, elem_test, op, x_path, id_list, iter, teston, is_skipped = False if testop in [ 'no-diff', 'list-not-less', 'list-not-more', 'delta']: - if check is True or action is "check": + if check is True or action == "check": xml1 = self.get_xml_reply(db, snap1) xml2 = self.get_xml_reply(db, snap2) if xml2 is None: @@ -255,7 +262,7 @@ def expression_evaluator(self, elem_test, op, x_path, id_list, iter, teston, else: # if check is used with uni operand test operator then use # second snapshot file - if check is True or action is "check": + if check is True or action == "check": pre_snap = self.get_xml_reply(db, snap1) post_snap = self.get_xml_reply(db, snap2) else: @@ -353,7 +360,7 @@ def expression_builder(self, sub_expr, parent_op=None, **kwargs): expr = '{0} {1}'.format(parent_op,ret_expr[0]) elif len(ret_expr) >= 1 : expr = ' {0} '.format(parent_op).join(ret_expr) - if expr is not '': + if expr != '': expr = '(' +expr+ ')' return expr @@ -383,7 +390,7 @@ def compare_reply( top_ignore_null = ignore_null_list[0].get('ignore-null') #### extract all test cases in given test file #### tests = [t for t in tests if ('iterate' in t or 'item' in t)] - if not len(tests) and (check is True or action is "check"): + if not len(tests) and (check is True or action == "check"): res = self.compare_xml(op, db, teston, snap1, snap2) if res is False: op.no_failed = op.no_failed + 1 @@ -436,7 +443,7 @@ def compare_reply( } final_boolean_expr = self.expression_builder(testcases, None, **kwargs) #for cases where skip was encountered due to ignore-null - if final_boolean_expr is '' or final_boolean_expr is None or final_boolean_expr == str(None): + if final_boolean_expr == '' or final_boolean_expr is None or final_boolean_expr == str(None): continue result = eval(final_boolean_expr) @@ -752,7 +759,7 @@ def generate_test_files( # if check is true then call function to compare two # snapshots #### - if (check is True or action is "check") and reply_format == 'xml': + if (check is True or action == "check") and reply_format == 'xml': if db.get('check_from_sqlite') is False: snapfile2 = self.generate_snap_file( device, diff --git a/lib/jnpr/jsnapy/jsnapy.py b/lib/jnpr/jsnapy/jsnapy.py index 65496810..68b8d970 100755 --- a/lib/jnpr/jsnapy/jsnapy.py +++ b/lib/jnpr/jsnapy/jsnapy.py @@ -263,7 +263,7 @@ def chk_database(self, config_file, pre_snapfile, "Specify name of the database.", extra=self.log_detail) exit(1) - if check is True or self.args.diff is True or action is "check": + if check == True or self.args.diff == True or action == "check": if 'compare' in list(d) and d['compare'] is not None: strr = d['compare'] if not isinstance(strr, str): @@ -369,39 +369,28 @@ def get_hosts(self): sys.exit(1) self.login(output_file) - def generate_rpc_reply(self, dev, output_file, hostname, config_data): + def generate_rpc_reply(self, dev, output_file, hostname, config_data, test_cases=None, **kwargs): """ Generates rpc-reply based on command/rpc given and stores them in snap_files :param dev: device handler :param output_file: filename to store snapshots :param hostname: hostname of device :param config_data : data of main config file + :param test_cases : list of test cases to be executed """ val = None test_files = [] - for tfile in config_data.get('tests'): - if not os.path.isfile(tfile): - tfile = os.path.join( - expanduser(get_path( - 'DEFAULT', - 'test_file_path')), - tfile) - if os.path.isfile(tfile): - test_file = open(tfile, 'r') - test_files.append(yaml.load(test_file, Loader=yaml.FullLoader)) - else: - self.logger.error( - colorama.Fore.RED + - "ERROR!! File %s is not found for taking snapshots" % - tfile, extra=self.log_detail) - - g = Parser() + if test_cases is not None: + test_files = test_cases + else : + test_files = self.extract_test_cases(config_data) + g = Parser(**kwargs) for tests in test_files: val = g.generate_reply(tests, dev, output_file, hostname, self.db) return val def compare_tests( - self, hostname, config_data, pre_snap=None, post_snap=None, action=None): + self, hostname, config_data, pre_snap=None, post_snap=None, action=None, **kwargs): """ called by check and snapcheck argument, to compare snap files calls the function to compare snapshots based on arguments given @@ -409,7 +398,7 @@ def compare_tests( :param hostname: device name :return: return object of Operator containing test details """ - comp = Comparator() + comp = Comparator(**kwargs) chk = self.args.check diff = self.args.diff pre_snap_file = self.args.pre_snapfile if pre_snap is None else pre_snap @@ -453,6 +442,9 @@ def login(self, output_file): :param output_file: name of snapshot file """ self.host_list = [] + config_data = self.main_file + if config_data is not None: + test_cases = self.extract_test_cases(config_data) if self.args.hostname is None: host_dict={} try: @@ -488,41 +480,30 @@ def login(self, output_file): gp = first_entry.get('group', 'all') dgroup = [i.strip().lower() for i in gp.split(',')] + iter = 0 for dgp in dev_file: if dgroup[0].lower() == 'all' or dgp.lower() in dgroup: for val in dev_file[dgp]: + # There can be multiple values of device/hostname + # The values can have same hostname but different port + # key for the dictionary modified from hostname to enumerate value to keep distinction hostname = list(val)[0] + iter += 1 self.log_detail = {'hostname': hostname} - if val.get(hostname) is not None and hostname not in host_dict: - host_dict[hostname] = deepcopy(val.get(hostname)) + if val.get(hostname) is not None and hostname not in self.host_list: + #host_dict[hostname] = deepcopy(val.get(hostname)) self.host_list.append(hostname) + host_dict[iter] = deepcopy(val.get(hostname)) + host_dict[iter]["device"] = hostname + # login credentials are given in main config file, can connect to multiple devices else: #key_value = deepcopy(k) - for host in hosts_val: - try: - hostname = host['device'] - self.log_detail = {'hostname': hostname} - except KeyError as ex: - self.logger.error( - colorama.Fore.RED + - "ERROR!! KeyError 'device' key not found", - extra=self.log_detail) - #raise Exception(ex) - except Exception as ex: - self.logger.error( - colorama.Fore.RED + - "ERROR!! %s" % - ex, - extra=self.log_detail) - #raise Exception(ex) - else: - if hostname not in host_dict: - self.host_list.append(hostname) - # host.pop('device') - host_dict[hostname] = deepcopy(host) - - for (hostname, key_value) in iteritems(host_dict): + self.get_hosts_list(hosts_val,host_dict) + + for (iter, key_value) in iteritems(host_dict): + hostname = key_value.get('device') + #it is under check that args.hostname is None #The file config takes precedence over cmd line params -- no changes made username = self.args.login or key_value.get('username') password = self.args.passwd or key_value.get('passwd') @@ -537,7 +518,8 @@ def login(self, output_file): hostname, username, password, - output_file + output_file, + test_cases ), kwargs= key_value ) @@ -554,9 +536,9 @@ def login(self, output_file): self.host_list.append(hostname) port = self.args.port key_value = {'port': port} if port is not None else {} - self.connect(hostname, username, password, output_file, **key_value) + self.connect(hostname, username, password, output_file, test_cases, **key_value) - def get_test(self, config_data, hostname, snap_file, post_snap, action): + def get_test(self, config_data, hostname, snap_file, post_snap, action, **kwargs): """ Analyse testfile and return object of operator.Operator containing test details called by connect() function and other functions of Jsnapy module functions @@ -574,7 +556,8 @@ def get_test(self, config_data, hostname, snap_file, post_snap, action): config_data, snap_file, post_snap, - action) + action, + **kwargs) result_status = res.result @@ -628,7 +611,32 @@ def get_test(self, config_data, hostname, snap_file, post_snap, action): self.q.put(res) return res - def connect(self, hostname, username, password, output_file, + def extract_test_cases(self, config_data): + """ + extract the test cases from the file and returns them + :param config_data: the data passed in the config file + :return: the list of testcases + """ + test_files = [] + for tfile in config_data.get('tests'): + # tfile gets details of the test/files to be parsed. + if not os.path.isfile(tfile): + tfile = os.path.join( + expanduser(get_path( + 'DEFAULT', + 'test_file_path')), + tfile) + if os.path.isfile(tfile): + test_file = open(tfile, 'r') + test_files.append(yaml.load(test_file, Loader=yaml.FullLoader)) + else: + self.logger.error( + colorama.Fore.RED + + "ERROR!! File %s is not found for taking snapshots" % + tfile, extra=self.log_detail) + return test_files + + def connect(self, hostname, username, password, output_file, testcases, config_data=None, action=None, post_snap=None, **kwargs): """ connect to device and calls the function either to generate snapshots @@ -647,7 +655,7 @@ def connect(self, hostname, username, password, output_file, if 'local' in config_data: self.args.local = True - if (self.args.snap is True or action is "snap") or ( (self.args.snapcheck is True or action is "snapcheck") and self.args.local is not True ): + if (self.args.snap is True or action == "snap") or ( (self.args.snapcheck is True or action == "snapcheck") and self.args.local is not True ): self.logger.info( colorama.Fore.BLUE + "Connecting to device %s ................", hostname, extra=self.log_detail) @@ -675,6 +683,7 @@ def connect(self, hostname, username, password, output_file, username, password, output_file, + testcases, config_data, action, post_snap, @@ -696,7 +705,9 @@ def connect(self, hostname, username, password, output_file, dev, output_file, hostname, - config_data) + config_data, + testcases, + **kwargs) self.snap_q.put(res) dev.close() if self.args.check is True or self.args.snapcheck is True or self.args.diff is True or action in [ @@ -715,7 +726,8 @@ def connect(self, hostname, username, password, output_file, hostname, local_snap, post_snap, - action) + action, + **kwargs) res[local_snap] = ret_obj else: res = self.get_test( @@ -723,8 +735,9 @@ def connect(self, hostname, username, password, output_file, hostname, output_file, post_snap, - action) - + action, + **kwargs) + return res ############################### functions to support module ############## @@ -760,36 +773,25 @@ def multiple_device_details( gp = first_entry.get('group', 'all') dgroup = [i.strip().lower() for i in gp.split(',')] + iter = 0 for dgp in dev_file: if dgroup[0].lower() == 'all' or dgp.lower() in dgroup: for val in dev_file[dgp]: hostname = list(val)[0] self.log_detail = {'hostname': hostname} - if val.get(hostname) is not None and hostname not in host_dict: - host_dict[hostname] = deepcopy(val.get(hostname)) + iter += 1 + if val.get(hostname) is not None and hostname not in self.host_list: self.host_list.append(hostname) + host_dict[iter] = deepcopy(val.get(hostname)) + host_dict[iter]["device"] = hostname else: - for host in hosts: - try: - hostname = host['device'] - self.log_detail = {'hostname': hostname} - except KeyError as ex: - self.logger.error( - colorama.Fore.RED + - "ERROR!! KeyError 'device' key not found", - extra=self.log_detail) - except Exception as ex: - self.logger.error( - colorama.Fore.RED + - "ERROR!! %s" % - ex, - extra=self.log_detail) - else: - if hostname not in host_dict: - self.host_list.append(hostname) - host_dict[hostname] = deepcopy(host) + # changes to support port + self.get_hosts_list(hosts, host_dict) + - for (hostname, key_value) in iteritems(host_dict): + test_cases = self.extract_test_cases(config_data) + for (iter, key_value) in iteritems(host_dict): + hostname = key_value.get('device') username = key_value.get('username') password = key_value.get('passwd') key_value = self.get_values(key_value) @@ -800,6 +802,7 @@ def multiple_device_details( username, password, pre_name, + test_cases, config_data, action, post_name), @@ -847,10 +850,10 @@ def extract_data( except Exception as ex: self.logger.error( colorama.Fore.RED + - "ERROR!! config file %s is not present" % + "ERROR!! hosts not defined in file or some error in data" % ex, extra=self.log_detail) - raise Exception("config file is not present ", ex) + raise Exception("Incorrect config file or data ", ex) else: self.args.local = local if config_data.__contains__( @@ -880,11 +883,13 @@ def extract_data( #pre_name = hostname + '_' + pre_name if not os.path.isfile(pre_name) else pre_name # if action is "check": # post_name= hostname + '_' + post_name if not os.path.isfile(post_name) else post_name + testcase = self.extract_test_cases(config_data) val.append(self.connect( hostname, username, password, pre_name, + testcase, config_data, action, post_name, @@ -941,7 +946,7 @@ def extract_dev_data( if 'local' in config_data: local = True - if action is "snap" or ( action is "snapcheck" and local is False ) : + if action == "snap" or ( action == "snapcheck" and local is False ) : try: res.append(self.generate_rpc_reply( dev, @@ -1033,6 +1038,36 @@ def check(self, data, pre_file=None, post_file=None, dev=None, folder=None): res = self.extract_data(data, pre_file, "check", post_file) return res + def get_hosts_list(self, hosts_val, host_dict): + """ + Function extracts list of hosts from the details given. + :param hosts_val: has the list of hosts to be parsed + :param host_dict: The dictionary to be created to store the parsed values + """ + + for counter, host in enumerate(hosts_val): + try: + hostname = host['device'] + self.log_detail = {'hostname': hostname} + except KeyError as ex: + self.logger.error( + colorama.Fore.RED + + "ERROR!! KeyError 'device' key not found", + extra=self.log_detail) + except Exception as ex: + self.logger.error( + colorama.Fore.RED + + "ERROR!! %s" % + ex, + extra=self.log_detail) + else: + if hostname not in self.host_list: + self.host_list.append(hostname) + # There can be multiple values of device/hostname + # The values can have same hostname but different port + # key for the dictionary modified from hostname to enumerate value to keep distinction + host_dict[counter] = deepcopy(host) + ####### generate init folder ###### ''' def generate_init(self): diff --git a/lib/jnpr/jsnapy/logging.yml b/lib/jnpr/jsnapy/logging.yml index 518b0ec9..6ae48fa6 100755 --- a/lib/jnpr/jsnapy/logging.yml +++ b/lib/jnpr/jsnapy/logging.yml @@ -11,6 +11,9 @@ formatters: format: "%(asctime)s - %(name)s - %(levelname)s - %(hostname)s ............. \n %(message)s" simple: format: "%(hostname)s-- %(message)s" + debug: + format: "%(message)s :::: [%(filename)s:%(lineno)d] [%(hostname)s] [%(levelname)s]" + #Helpful in debugging issues in the code default: format: "%(message)s" default_file: diff --git a/lib/jnpr/jsnapy/operator.py b/lib/jnpr/jsnapy/operator.py index 49a8963e..b82cc291 100755 --- a/lib/jnpr/jsnapy/operator.py +++ b/lib/jnpr/jsnapy/operator.py @@ -2294,10 +2294,11 @@ def no_diff(self, x_path, ele_list, err_mssg, ele_xpath1 = data1.get(k).xpath(ele_list[0]) ele_xpath2 = data2.get(k).xpath(ele_list[0]) - val_list1 = [element.text.strip() for element in ele_xpath1] if len( + + val_list1 = [element.text.strip() if element.text else None for element in ele_xpath1] if len( + ele_xpath1) != 0 else None + val_list2 = [element.text.strip() if element.text else None for element in ele_xpath1] if len( ele_xpath1) != 0 else None - val_list2 = [element.text.strip() for element in ele_xpath2] if len( - ele_xpath2) != 0 else None predict[ele_list[0]] = val_list1 postdict[ele_list[0]] = val_list2 @@ -2386,6 +2387,11 @@ def no_diff(self, x_path, ele_list, err_mssg, def list_not_less( self, x_path, ele_list, err_mssg, info_mssg, teston, iter, id_list, test_name, xml1, xml2, ignore_null=None): + """ + Checks if all the occurrence of the passed XML element is present in the first snapshot but not present in + second snapshot. + if ignore-null is set to true and element is not present in first snapshot, the test will be skipped + """ self.print_testmssg("list-not-less") res = True tresult = { @@ -2406,9 +2412,7 @@ def list_not_less( id_val = {} pre_nodes, post_nodes = self._find_xpath(iter, x_path, xml1, xml2) - - if not pre_nodes or not post_nodes: - + if not pre_nodes: if self._is_ignore_null(ignore_null): self.logger_testop.debug(colorama.Fore.YELLOW + "SKIPPING!! Nodes are not present in given Xpath: <{}>".format( @@ -2497,6 +2501,7 @@ def list_not_less( deepcopy(node_value_failed)) else: + postdict[ele_list[0]] = val1 count_pass = count_pass + 1 message = self._print_message( info_mssg, @@ -2584,6 +2589,11 @@ def list_not_less( def list_not_more( self, x_path, ele_list, err_mssg, info_mssg, teston, iter, id_list, test_name, xml1, xml2, ignore_null=None): + """ + Checks if all the occurrence of the passed XML element is present in the second snapshot but not present in + first snapshot. + if ignore-null is set to true and element is not present in second snapshot, the test will be skipped + """ self.print_testmssg("list-not-more") res = True tresult = { @@ -2604,7 +2614,8 @@ def list_not_more( id_val = {} pre_nodes, post_nodes = self._find_xpath(iter, x_path, xml1, xml2) - if not pre_nodes or not post_nodes: + + if not post_nodes: if self._is_ignore_null(ignore_null): self.logger_testop.debug(colorama.Fore.YELLOW + @@ -2632,7 +2643,7 @@ def list_not_more( predata = self._get_data(id_list, pre_nodes, ignore_null) postdata = self._get_data(id_list, post_nodes, ignore_null) - if not predata: + if not postdata: self.logger_testop.debug(colorama.Fore.YELLOW + "SKIPPING!! Nodes are not present for given IDs: {}".format( id_list), @@ -2690,6 +2701,7 @@ def list_not_more( ele_xpath2[0].getparent().tag), extra=self.log_detail) else: count_pass = count_pass + 1 + predict[ele_list[0]] = val2 message = self._print_message( info_mssg, iddict, diff --git a/lib/jnpr/jsnapy/setup_logging.py b/lib/jnpr/jsnapy/setup_logging.py index ff4f8840..eff48d02 100755 --- a/lib/jnpr/jsnapy/setup_logging.py +++ b/lib/jnpr/jsnapy/setup_logging.py @@ -10,6 +10,7 @@ import logging.config from jnpr.jsnapy import get_config_location from jnpr.jsnapy import get_path +from jnpr.jsnapy import venv_check import sys def setup_logging( @@ -30,7 +31,8 @@ def setup_logging( ################################### # Creating Folder path for SNAPSHOT ################################### - if 'win32' not in sys.platform and not hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 24-29 in the __init__.py. + if 'win32' not in sys.platform and not venv_check: snapshots_path = get_path('DEFAULT', 'snapshot_path') snapshots_path = os.path.expanduser(snapshots_path) if not os.path.isdir(snapshots_path): diff --git a/lib/jnpr/jsnapy/snap.py b/lib/jnpr/jsnapy/snap.py index 92a5326a..aaa2cf01 100755 --- a/lib/jnpr/jsnapy/snap.py +++ b/lib/jnpr/jsnapy/snap.py @@ -22,13 +22,19 @@ class Parser: - def __init__(self): + def __init__(self, **kwargs): + """ + Parser object constructor. + :param int port. + + """ self.logger_snap = logging.getLogger(__name__) self.log_detail = {'hostname': None} self.reply = {} self.command_list = [] self.rpc_list = [] self.test_included = [] + self.port = kwargs.get('port', None) def _write_file(self, rpc_reply, format, output_file): """ @@ -89,6 +95,8 @@ def generate_snap_file(self, output_file, hostname, name, cmd_format): :param cmd_format: xml/text :return: return output file """ + if self.port is not None: + hostname = "{}_{}".format(hostname, self.port) name = name.split('|')[0].strip() cmd_rpc = re.sub('/|\*|\.|-|\|', '_', name) if os.path.isfile(output_file): diff --git a/lib/jnpr/jsnapy/version.py b/lib/jnpr/jsnapy/version.py index 13526909..19646f3d 100755 --- a/lib/jnpr/jsnapy/version.py +++ b/lib/jnpr/jsnapy/version.py @@ -5,5 +5,5 @@ # All rights reserved. # -__version__ = "1.3.2" -DATE = "2018-May-31" +__version__ = "1.3.3.dev0" +DATE = "2018-June-01" diff --git a/samples/compliance_checks.yml b/samples/compliance_checks.yml new file mode 100644 index 00000000..3861490d --- /dev/null +++ b/samples/compliance_checks.yml @@ -0,0 +1,431 @@ +bgp_authentication_check: + - command: show bgp neighbor + - iterate: + xpath: '//bgp-option-information' + tests: + - exists: authentication-configured + info: "Test Succeeded!! BGP Authentication is set for <{{post['..//peer-address']}}>." + err: "Test Failed!!! BGP Authentication is NOT set for <{{post['..//peer-address']}}>!!!" + +re_filter_compliance_check: + - command: show interfaces lo0.0 detail + - iterate: + xpath: /interface-information/logical-interface/address-family + tests: + - exists: filter-information/filter-input + err: "Test Failed!! Firewall-input-filter on Routing-Engine is NOT set!!!" + info: "Test succeeded!! Firewall-input-filter on Routing-Engine is set to <{{post['filter-input']}}>." + +interface_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/interfaces + - iterate: + xpath: /configuration/interfaces/interface/unit + tests: + - not-exists: proxy-arp + err: "Test Failed!! Proxy-arp is set on interface <{{post['../name']}}>!!!" + info: "Test succeeded!! Proxy-arp is not set." + - iterate: + xpath: /configuration/interfaces/interface/unit + tests: + - not-exists: family/inet/targeted-broadcast + err: "Test Failed!! Targeted-broadcast is set on interface <{{post['../name']}}>!!!" + info: "Test succeeded!! Targeted-broadcast is not set." + +arp_policer_check: + - command: show policer + - iterate: + xpath: /firewall-information/filter-information/policer + tests: + - exists: policer-name, __default_arp_policer__ + err: "Default ARP Policer is NOT active. Check is there is a custom policer defined!" + info: "Default ARP Policer {{post['policer-name']}} is active" + +insecure_services_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - iterate: + xpath: //services + tests: + - not-exists: //rsh + info: "Test Succeeded!! Rsh Service is not enabled." + err: "Test Failed!!! Insecure service rsh is enabled !!" + - not-exists: //rlogin + info: "Test Succeeded!! Rlogin Service is not enabled." + err: "Test Failed!!! Insecure service rlogin is enabled !!" + - not-exists: //ftp + err: "Test Failed!!! Insecure service ftp is enabled !!" + info: "Test Succeeded!! Ftp Service is not enabled." + - not-exists: //finger + err: "Test Failed!!! Insecure service finger is enabled !!" + info: "Test Succeeded!! Finger Service is not enabled." + - not-exists: //telnet + err: "Test Failed!!! Insecure service telnet is enabled !!" + info: "Test Succeeded!! Telnet Service is not enabled." + - not-exists: //xnm-clear-text + err: "Test Failed!!! Insecure service xnm-clear-text is enabled !!" + info: "Test Succeeded!! Xnm-clear-text Service is not enabled." + - not-exists: //web-management + err: "Test Failed!!! Insecure service web-management is enabled !!" + info: "Test Succeeded!! Web-management Service is not enabled." + - not-exists: //tftp-server + err: "Test Failed!!! Insecure service tftp-server is enabled !!" + info: "Test Succeeded!! Tftp-server is not enabled." + - not-exists: //rest + err: "Test Failed!!! Insecure service rest is enabled !!" + info: "Test Succeeded!! Rest is not enabled." + - not-exists: //dhcp-local-server + err: "Test Failed!!! Insecure dhcp-local-server is enabled !!" + info: "Test Succeeded!! Dhcp-local-server is not enabled." + - not-exists: //service-deployment + err: "Test Failed!!! Insecure service service-deployment is enabled !!" + info: "Test Succeeded!! Service-deployment is not enabled." + - not-exists: //webapi + err: "Test Failed!!! Insecure service webapi is enabled !!" + info: "Test Succeeded!! Webapi is not enabled." + - not-exists: //xnm-ssl + err: "Test Failed!!! Insecure service xnm-ssl is enabled !!" + info: "Test Succeeded!! Xnm-ssl is not enabled." + - not-exists: //dns + err: "Test Failed!!! Insecure service dns is enabled !!" + info: "Test Succeeded!! Dns-server is not enabled." + - not-exists: //dynamic-dns + err: "Test Failed!!! Insecure service dynamic-dns is enabled !!" + info: "Test Succeeded!! Dynamic-dns is not enabled." + +ssh_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system/services + - item: + xpath: //ssh + tests: + - is-equal: //root-login, deny + info: "Test Succeeded!! Root login is equal to <{{post['//root-login']}}>." + err: "Test Failed!!!, root-login is not equal to deny, it is <{{post['//root-login']}}>." + - is-equal: //protocol-version, v2 + info: "Test Succeeded!! SSH Protocol version is equal to <{{post['//protocol-version']}}>." + err: "Test Failed!!! SSH Protocol version it not equal to v2, it is <{{post['//protocol-version']}}>." + - exists: //no-tcp-forwarding + info: "Test Succeeded!! SSH TCP Forwarding is disabled." + err: "Test Failed!!! SSH TCP Forwarding is enabled!!!" + - exists: //connection-limit + info: "Test Succeeded!! SSH connection-limit is enabled with a value of <{{post['//connection-limit']}}>." + err: "Test Failed!!! There is no SSH connection-limit configured!!!" + - exists: //rate-limit + info: "Test Succeeded!! SSH rate-limit is enabled with a value of <{{post['//rate-limit']}}>." + err: "Test Failed!!! There is no SSH rate-limit configured!!!" + - exists: //max-sessions-per-connection + info: "Test Succeeded!! SSH max-sessions-per-connection is enabled with a value of <{{post['//max-sessions-per-connection']}}>." + err: "Test Failed!!! There is no SSH max-sessions-per-connection configured!!!" + - exists: //client-alive-interval + info: "Test Succeeded!! SSH client-alive-interval is enabled with a value of <{{post['//client-alive-interval']}}>." + err: "Test Failed!!! There is no SSH client-alive-interval configured!!!" + - exists: //client-alive-count-max + info: "Test Succeeded!! SSH client-alive-count-max is enabled with a value of <{{post['//client-alive-count-max']}}>." + err: "Test Failed!!! There is no SSH client-alive-count-max configured!!!" + +netconf_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system/services + - item: + xpath: //netconf + tests: + - exists: //connection-limit + info: "Test Succeeded!! SSH connection-limit is enabled with a value of <{{post['//connection-limit']}}>." + err: "Test Failed!!! There is no SSH connection-limit configured!!!" + - exists: //rate-limit + info: "Test Succeeded!! SSH rate-limit is enabled with a value of <{{post['//rate-limit']}}>." + err: "Test Failed!!! There is no SSH rate-limit configured!!!" + +snmp_compliance_check: + - rpc: get-config + - item: + xpath: //snmp + tests: + - OR: + - contains: interface, me + info: "Test Succeeded!! SNMPv3 interface is set." + err: "Test Failed!!! SNMPv3 is not set to OOB management only!!!" + - contains: interface, em + info: "Test Succeeded!! SNMPv3 interface is set." + err: "Test Failed!!! SNMPv3 is not set to OOB management only!!!" + - contains: interface, fxp + info: "Test Succeeded!! SNMPv3 interface is set." + err: "Test Failed!!! SNMPv3 is not set to OOB management only!!!" + - iterate: + xpath: /configuration/snmp/community + tests: + - not-in: name, public,private,admin,monitor,security + info: "Test Succeeded!! Common snmp community strings are not used." + err: "Test Failed!!! Common snmp community strings are in use <{{post['name']}}>!!!" + - iterate: + xpath: /configuration/snmp/v3/usm/local-engine/user + tests: + - exists: authentication-sha + info: "Test Succeeded!! SNMPv3 authentication-sha is set." + err: "Test Failed!!! Authentication-sha is not set for SNMPv3 user <{{post['name']}}>!!!" + - exists: privacy-aes128 + info: "Test Succeeded!! SNMPv3 privacy-aes128 is set." + err: "Test Failed!!! Privacy-aes128 is not set for SNMPv3 user <{{post['name']}}>!!!" + +aaa_compliance_check: + - rpc: get-config + - item: + xpath: /configuration/system + tests: + - contains: //authentication-order[1], tacplus, radius + info: "Test Succeeded!! Tacplus or radius are used as external AAA." + err: "Test Failed!!! Neither tacplus nor radius are used for external AAA!!!" + - iterate: + xpath: /configuration/system/tacplus-server + tests: + - exists: secret + info: "Test Succeeded!! Secret is set for tacplus-server <{{post['name[1]']}}>." + err: "Test Failed!!! Secret is NOT set for tacplus-server <{{post['name[1]']}}>!!!" + - exists: source-address + info: "Test Succeeded!! Source-address is set for tacplus-server <{{post['name[1]']}}>." + err: "Test Failed!!! Source-address is NOT set for tacplus-server <{{post['name[1]']}}>!!!" + +login_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - item: + xpath: /configuration/system/login + tests: + - exists: //message + info: "Test Succeeded!! Login message is set." + err: "Test Failed!!! No login message set!!!" + - iterate: + xpath: /configuration/system/login/user + tests: + - exists: class + info: "Test Succeeded!! Login classes for all users set." + err: "Test Failed!!! No login class for user <{{post['name']}}>!!!" + - not-in: class, super-user, read-only, operator, unauthorized + info: "Test Succeeded!! No predefined login classes in use." + err: "Test Failed!!! Predefined login class in use for user <{{post['name']}}>!!!" + - item: + xpath: /configuration/system/login/password + tests: + - is-equal: format, sha512 + info: "Test Succeeded!! Password Login Format is set to sha512." + err: "Test Failed!!! Password Login Format is NOT set to sha512, it is <{{post['format']}}>!!!" + - is-equal: change-type, character-sets + info: "Test Succeeded!! Local passwords are configured to require character set changes" + err: "Test Failed!!! Local passwords are NOT configured to require character set changes!!!" + - is-gt: minimum-length, 9 + info: "Test Succeeded!! Local passwords are limited to at least 10 characters.It is set to <{{post['minimum-length']}}>" + err: "Test Failed!!! Local passwords are NOT limited to at least 10 characters, set to <{{post['minimum-length']}}>!!!" + - is-gt: minimum-changes, 3 + info: "Test Succeeded!! Local passwords require at least 4 character set changes. It is set to <{{post['minimum-changes']}}>" + err: "Test Failed!!! Local passwords do NOT require at least 4 character set changes, set to <{{post['minimum-changes']}}>!!!" + - item: + xpath: /configuration/system/root-authentication + tests: + - exists: no-public-keys + info: "Test Succeeded!! Public-key authentication is disabled for root account." + err: "Test Failed!!! Public-key authentication is possible for root account!!!" + +ntp_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system/ntp + - item: + xpath: /configuration/system/ntp + tests: + - exists: //boot-server + info: "Test Succeeded!! NTP boot-server is set." + err: "Test Failed!!! NTP boot-server is NOT set!!!" + - exists: //server[1]/name + info: "Test Succeeded!! First NTP is set to <{{post['//server[1]/name']}}>." + err: "Test Failed!!! No NTP Server is set!!!" + - exists: //server[2]/name + info: "Test Succeeded!! Second NTP is set to <{{post['//server[2]/name']}}>." + err: "Test Failed!!! No Second NTP server is NOT set!!!" + - exists: //authentication-key + info: "Test Succeeded!! NTP Authentication Key is set." + err: "Test Failed!!! NTP Authentication Key is NOT set!!!" + - iterate: + xpath: //server + tests: + - is-equal: version, 4 + info: "Test Succeeded!! NTP Version for server <{{post['name']}}> is equal to 4." + err: "Test Failed!!! NTP Version for server <{{post['name']}}> is NOT equal to 4, it is: <{{post['version']}}>!!!" + +login_retries_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system/login/retry-options + - item: + xpath: /configuration/system/login/retry-options + tests: + - exists: tries-before-disconnect + err: "Test Failed!! System login retry-options tries-before-disconnect NOT set!!!" + info: "Test succeeded!! System login retry-options tries-before-disconnect is set to <{{post['tries-before-disconnect']}}>" + - exists: backoff-threshold + err: "Test Failed!! System login retry-options backoff-threshold NOT set!!!" + info: "Test succeeded!! System login retry-options backoff-threshold is set to <{{post['backoff-threshold']}}>" + - exists: backoff-factor + err: "Test Failed!! System login retry-options backoff-factorNOT set!!!" + info: "Test succeeded!! System login retry-options backoff-factor is set to <{{post['backoff-factor']}}>" + - exists: minimum-time + err: "Test Failed!! System login retry-options minimum-time NOT set!!!" + info: "Test succeeded!! System login retry-options minimum-time is set to <{{post['minimum-time']}}>" + - exists: maximum-time + err: "Test Failed!! System login retry-options maximum-time NOT set!!!" + info: "Test succeeded!! System login retry-options maximum-time is set to <{{post['maximum-time']}}>" + - exists: lockout-period + err: "Test Failed!! System login retry-options lockout-period NOT set!!!" + info: "Test succeeded!! System login retry-options lockout-period is set to <{{post['lockout-period']}}>" + +system_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - item: + xpath: /configuration/system + tests: + - exists: no-multicast-echo + err: "Test Failed!! System no-multicast-echo NOT set!!!" + info: "Test succeeded!! System no-multicast-echo is set." + - exists: no-redirects + err: "Test Failed!! System no-redirects NOT set!!!" + info: "Test succeeded!! System no-redirects is set." + - exists: no-redirects-ipv6 + err: "Test Failed!! System no-redirects-ipv6 NOT set!!!" + info: "Test succeeded!! System no-redirects-ipv6 is set." + - exists: no-ping-record-route + err: "Test Failed!! System no-ping-record-route NOT set!!!" + info: "Test succeeded!! System no-ping-record-route is set." + - exists: no-ping-time-stamp + err: "Test Failed!! System no-ping-time-stamp NOT set!!!" + info: "Test succeeded!! System no-ping-time-stamp is set." + - exists: default-address-selection + err: "Test Failed!! System default-address-selection NOT set!!!" + info: "Test succeeded!! System default-address-selection is set." + - is-equal: time-zone, Europe/Berlin + err: "Test Failed!! System timezone NOT set to Europe/Berlin, it is set to <{{post['time-zone']}}>!!!" + info: "Test succeeded!! System timezone is set to <{{post['time-zone']}}>." + - not-contains: host-name, juniper + err: "Test Failed!! System hostname contains device make or model <{{post['host-name']}}>!!!" + info: "Test succeeded!! System hostname does not contain device make or model <{{post['time-zone']}}>." + - not-contains: host-name, srx + err: "Test Failed!! System hostname contains device make or model <{{post['host-name']}}>!!!" + info: "Test succeeded!! System hostname does not contain device make or model <{{post['time-zone']}}>." + - item: + xpath: /configuration/system + tests: + - exists: accounting + err: "Test Failed!! System accounting NOT set!!!" + info: "Test succeeded!! System accounting is set." + - exists: accounting/destination + err: "Test Failed!! System accounting destination NOT set!!!" + info: "Test succeeded!! System accounting destination is set." + - item: + xpath: /configuration/system/accounting + tests: + - is-equal: events[1], login + err: "Test Failed!! System accounting of logins NOT set!!!" + info: "Test succeeded!! System accounting of logins is set." + - is-equal: events[2], change-log + err: "Test Failed!! System accounting of configuration changes NOT set!!!" + info: "Test succeeded!! System accounting of configuration changes is set." + - is-equal: events[3], interactive-commands + err: "Test Failed!! System accounting of interactive commands NOT set!!!" + info: "Test succeeded!! System accounting of interactive commands is set." + - iterate: + xpath: /configuration/system/archival/configuration + tests: + - exists: transfer-on-commit + err: "Test Failed!! System archival on commit NOT set!!!" + info: "Test succeeded!! System archival on commit is set." + - contains: archive-sites/name, scp:// + err: "Test Failed!! System archive-site NOT set to scp!!!" + info: "Test succeeded!! System archive-site is set to scp." + +system_internet_options_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system/internet-options + - item: + xpath: /configuration/system/internet-options + tests: + - exists: icmpv4-rate-limit + err: "Test Failed!! System internet-options icmpv4-rate-limit NOT set!!!" + info: "Test succeeded!! System internet-options icmpv4-rate-limit is set." + - exists: icmpv6-rate-limit + err: "Test Failed!! System internet-options icmpv6-rate-limit NOT set!!!" + info: "Test succeeded!! System internet-options icmpv6-rate-limit is set." + - exists: no-source-quench + err: "Test Failed!! System internet-options no-source-quench NOT set!!!" + info: "Test succeeded!! System internet-options no-source-quench is set." + - exists: no-tcp-reset + err: "Test Failed!! System internet-options no-tcp-reset NOT set!!!" + info: "Test succeeded!! System internet-options no-tcp-reset is set." + - exists: tcp-drop-synfin-set + err: "Test Failed!! System internet-options tcp-drop-synfin-set NOT set!!!" + info: "Test succeeded!! System internet-options tcp-drop-synfin-set is set." + +console_port_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - item: + xpath: /configuration/system + tests: + - OR: + - AND: + - exists: //ports/console/log-out-on-disconnect + err: "Test Failed!! System console port log-out-on-disconnect NOT set!!!" + info: "Test succeeded!! System console port log-out-on-disconnect is set." + - exists: //ports/console/insecure + err: "Test Failed!! System console port insecure NOT set!!!" + info: "Test succeeded!! System console port insecure is set." + # Necessary because 'or' operator needs more than 1 statement... + - OR: + - exists: //ports/console/disable + err: "Test Failed!! System console port is NOT disabled!!!" + info: "Test succeeded!! System console port is disabled." + - exists: //ports/console/disable + err: "Test Failed!! System console port is NOT disabled!!!" + info: "Test succeeded!! System console port is disabled." + +auxiliary_port_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - item: + xpath: /configuration/system + tests: + - OR: + - exists: //ports/auxiliary/disable + err: "Test Failed!! System auxiliary port is NOT disabled!!!" + info: "Test succeeded!! System auxiliary port is disabled." + - exists: //ports/auxiliary/insecure + err: "Test Failed!! System auxiliary port insecure NOT set!!!" + info: "Test succeeded!! System auxiliary port insecure is set." + +diag_and_pic_ports_compliance_check: + - rpc: get-config + - kwargs: + filter_xml: configuration/system + - item: + xpath: /configuration/system + tests: + - exists: //diag-port-authentication + err: "Test Failed!! System diag-port-authentication is NOT set!!!" + info: "Test succeeded!! System diag-port-authentication is set." + - exists: //diag-port-authentication/encrypted-password + err: "Test Failed!! System diag-port-authentication is NOT set to encrypted-password!!!" + info: "Test succeeded!! System diag-port-authentication is set to encrypted-password." + - exists: //pic-console-authentication + err: "Test Failed!! System pic-console-authentication is NOT set!!!" + info: "Test succeeded!! System pic-console-authentication is set." + - exists: //pic-console-authentication/encrypted-password + err: "Test Failed!! System pic-console-authentication is NOT set to encrypted-password!!!" + info: "Test succeeded!! System pic-console-authentication is set to encrypted-password." diff --git a/setup.py b/setup.py index bbd625d2..eb04e0c0 100755 --- a/setup.py +++ b/setup.py @@ -15,6 +15,14 @@ else: from configparser import ConfigParser +# Function added by @gcasella +# To check if the user is currently running the installation inside of a virtual environment that was installed using the `python3 -m venv venv` command. +def venv_check(): + + if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): + return True + else: + return False def set_logging_path(path): if os.path.exists(path): @@ -27,7 +35,8 @@ def set_logging_path(path): if handler == 'console': pass else: - if hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 20-25. + if venv_check() is True: value['filename'] = (os.path.join (sys.prefix, 'var/logs/jsnapy/jsnapy.log')) @@ -52,7 +61,8 @@ def run(self): # hasattr(sys,'real_prefix') checks whether the # user is working in python virtual environment # -------------------------------- - if hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 20-25. + if venv_check() is True: self.install_data = os.path.join(sys.prefix, 'etc', 'jsnapy') elif 'win32' in sys.platform: @@ -65,7 +75,8 @@ def run(self): mode = 0o777 install.run(self) - if 'win32' not in sys.platform and not hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 20-25. + if 'win32' not in sys.platform and not venv_check(): dir_mode = 0o755 file_mode = 0o644 os.chmod(dir_path, dir_mode) @@ -96,7 +107,8 @@ def run(self): config.set('DEFAULT', 'test_file_path', os.path.join(dir_path, 'testfiles')) - if hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines 20-25. + if venv_check() is True: default_config_location = os.path.join(sys.prefix, 'etc', 'jsnapy', 'jsnapy.cfg') @@ -123,8 +135,9 @@ def run(self): else: raise Exception('jsnapy.cfg not found at ' + default_config_location) - - if hasattr(sys, 'real_prefix'): + + # Modified by @gcasella to use the function created on lines 20-25. + if venv_check() is True: path = os.path.join(sys.prefix, 'etc', 'jsnapy', 'logging.yml') set_logging_path(path) elif 'win32' in sys.platform: @@ -149,7 +162,8 @@ def run(self): # Specifying only 'samples' means 'install_data Path'/samples # ---------------------------- -if hasattr(sys, 'real_prefix'): +# Modified by @gcasella to use the function created on lines 20-25. +if venv_check() is True: os_data_file = [('.', ['lib/jnpr/jsnapy/logging.yml']), ('../../var/logs/jsnapy', log_files), ('.', ['lib/jnpr/jsnapy/jsnapy.cfg']), diff --git a/tests/unit/configs/1.1.1.1_snap_exists_post_show_chassis_alarms.xml b/tests/unit/configs/1.1.1.1_snap_exists_post_show_chassis_alarms.xml new file mode 100644 index 00000000..5a2d08e3 --- /dev/null +++ b/tests/unit/configs/1.1.1.1_snap_exists_post_show_chassis_alarms.xml @@ -0,0 +1,14 @@ + + +2 + + + +2019-12-19 14:03:37 CEST + +Minor +Plane 0 not online +Plane 0 not online +Chassis + + \ No newline at end of file diff --git a/tests/unit/configs/list-not-less_ignore-null_skip_2.yml b/tests/unit/configs/list-not-less_ignore-null_skip_2.yml new file mode 100644 index 00000000..ddb974d8 --- /dev/null +++ b/tests/unit/configs/list-not-less_ignore-null_skip_2.yml @@ -0,0 +1,17 @@ +tests_include: + - test_command_version + +# use '/' in your test cases apart from xpath if u want to search all elements irrespective of hierarchy, ex: in id if u use /name instead of name +# then it will search in all the names in given xpath irrespective of their position +# for simple, one test using command + +test_command_version: + - command: show chassis alarms + - iterate: + xpath: alarm-detail + id: alarm-description + tests: + - list-not-less: alarm-short-description + err: "Interface {{pre['name']}} is missing in post snapshot" + info: "Interface {{pre['name']}} is present in post snapshot" + ignore-null: True diff --git a/tests/unit/configs/list-not-more_ignore-null_skip_2.yml b/tests/unit/configs/list-not-more_ignore-null_skip_2.yml new file mode 100644 index 00000000..a853bd5b --- /dev/null +++ b/tests/unit/configs/list-not-more_ignore-null_skip_2.yml @@ -0,0 +1,17 @@ +tests_include: + - test_command_version + +# use '/' in your test cases apart from xpath if u want to search all elements irrespective of hierarchy, ex: in id if u use /name instead of name +# then it will search in all the names in given xpath irrespective of their position +# for simple, one test using command + +test_command_version: + - command: show chassis alarms + - iterate: + xpath: alarm-detail + id: alarm-description + tests: + - list-not-more: alarm-short-description + err: "Interface {{pre['name']}} is missing in post snapshot" + info: "Interface {{pre['name']}} is present in post snapshot" + ignore-null: True diff --git a/tests/unit/configs/main_list-not-less_ignore-null_skip_2.yml b/tests/unit/configs/main_list-not-less_ignore-null_skip_2.yml new file mode 100644 index 00000000..39b36bf9 --- /dev/null +++ b/tests/unit/configs/main_list-not-less_ignore-null_skip_2.yml @@ -0,0 +1,14 @@ + +# for one device, can be given like this: +hosts: + - device: 1.1.1.1 + username : abc + passwd: xyz +tests: + - list-not-less_ignore-null_skip_2.yml + +sqlite: + - store_in_sqlite: yes + check_from_sqlite: no + database_name: jbb.db + diff --git a/tests/unit/configs/main_list-not-more_ignore-null_skip_2.yml b/tests/unit/configs/main_list-not-more_ignore-null_skip_2.yml new file mode 100644 index 00000000..8b9d9e71 --- /dev/null +++ b/tests/unit/configs/main_list-not-more_ignore-null_skip_2.yml @@ -0,0 +1,14 @@ + +# for one device, can be given like this: +hosts: + - device: 1.1.1.1 + username : abc + passwd: xyz +tests: + - list-not-more_ignore-null_skip_2.yml + +sqlite: + - store_in_sqlite: yes + check_from_sqlite: no + database_name: jbb.db + diff --git a/tests/unit/test_comparison_op.py b/tests/unit/test_comparison_op.py index ebd05b9e..f943298e 100644 --- a/tests/unit/test_comparison_op.py +++ b/tests/unit/test_comparison_op.py @@ -131,6 +131,54 @@ def test_list_not_less_ignore_null_skip_1(self, mock_path): self.assertEqual(oper.no_passed, 1) # even if no nodes is found, comparison between null and null passes self.assertEqual(oper.no_failed, 1) + @patch('jnpr.jsnapy.check.get_path') + def test_list_not_less_ignore_null_true_1(self, mock_path): + # Test to check if xml element not present in first snapshot and if ignore-NULL + # flag is set, it should ignore the test and move ahead. + self.chk = True + comp = Comparator() + conf_file = os.path.join(os.path.dirname(__file__), + 'configs', 'main_list-not-less_ignore-null_skip_2.yml') + mock_path.return_value = os.path.join(os.path.dirname(__file__), 'configs') + config_file = open(conf_file, 'r') + main_file = yaml.load(config_file, Loader=yaml.FullLoader) + oper = comp.generate_test_files( + main_file, + self.hostname, + self.chk, + self.diff, + self.db, + self.snap_del, + "snap_exists_pre", + self.action, + "snap_exists_post") + self.assertEqual(oper.no_passed, 0) + self.assertEqual(oper.no_failed, 0) + + @patch('jnpr.jsnapy.check.get_path') + def test_list_not_less_ignore_null_true_2(self, mock_path): + #Test to check if xml element is present in first snapshot but not present in second snapshot and if ignore-NULL + #flag is set, it should return failure in the test + self.chk = True + comp = Comparator() + conf_file = os.path.join(os.path.dirname(__file__), + 'configs', 'main_list-not-less_ignore-null_skip_2.yml') + mock_path.return_value = os.path.join(os.path.dirname(__file__), 'configs') + config_file = open(conf_file, 'r') + main_file = yaml.load(config_file, Loader=yaml.FullLoader) + oper = comp.generate_test_files( + main_file, + self.hostname, + self.chk, + self.diff, + self.db, + self.snap_del, + "snap_exists_post", + self.action, + "snap_exists_pre") + self.assertEqual(oper.no_passed, 0) + self.assertEqual(oper.no_failed, 1) + @patch('jnpr.jsnapy.check.get_path') def test_list_not_less_ignore_null_id_skip(self, mock_path): self.chk = True @@ -197,8 +245,6 @@ def test_list_not_more_fail(self, mock_path): self.assertEqual(oper.no_passed, 1) self.assertEqual(oper.no_failed, 1) - - @patch('jnpr.jsnapy.check.get_path') def test_list_not_more_pass(self, mock_path): self.chk = True @@ -267,6 +313,8 @@ def test_list_not_more_ignore_null_fail_1(self, mock_path): @patch('jnpr.jsnapy.check.get_path') def test_list_not_more_ignore_null_skip(self, mock_path): + # Test to check if xml element not present in second snapshot if ignore-NULL + # flag is set, it should ignore the test and move ahead. self.chk = True comp = Comparator() conf_file = os.path.join(os.path.dirname(__file__), @@ -287,6 +335,30 @@ def test_list_not_more_ignore_null_skip(self, mock_path): self.assertEqual(oper.no_passed, 0) self.assertEqual(oper.no_failed, 0) + @patch('jnpr.jsnapy.check.get_path') + def test_list_not_more_ignore_null_skip_2(self, mock_path): + #Test to check if xml element present in second snapshot but not present in second snapshot and if ignore-NULL + #flag is set, it should return failure. + self.chk = True + comp = Comparator() + conf_file = os.path.join(os.path.dirname(__file__), + 'configs', 'main_list-not-more_ignore-null_skip_2.yml') + mock_path.return_value = os.path.join(os.path.dirname(__file__), 'configs') + config_file = open(conf_file, 'r') + main_file = yaml.load(config_file, Loader=yaml.FullLoader) + oper = comp.generate_test_files( + main_file, + self.hostname, + self.chk, + self.diff, + self.db, + self.snap_del, + "snap_exists_pre", + self.action, + "snap_exists_post") + self.assertEqual(oper.no_passed, 0) + self.assertEqual(oper.no_failed, 1) + @patch('jnpr.jsnapy.check.get_path') def test_list_not_more_ignore_null_skip_1(self, mock_path): self.chk = True @@ -503,8 +575,8 @@ def test_no_diff(self, mock_path): "snap_no-diff_pre", self.action, "snap_no-diff_post") - self.assertEqual(oper.no_passed, 2) - self.assertEqual(oper.no_failed, 4) + self.assertEqual(oper.no_passed, 3) + self.assertEqual(oper.no_failed, 3) @patch('jnpr.jsnapy.check.get_path') def test_no_diff_2(self, mock_path): diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index 4c147210..a87b3cd5 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -4,7 +4,7 @@ import yaml from mock import patch, MagicMock from nose.plugins.attrib import attr -from jnpr.jsnapy import get_config_location, get_path, DirStore +from jnpr.jsnapy import get_config_location, get_path, DirStore, venv_check @attr('unit') class TestCheck(unittest.TestCase): @@ -36,7 +36,8 @@ def test_config_location_home(self, mock_is_file): @patch('os.path.isfile') def test_config_location_etc(self, mock_is_file): - if hasattr(sys, 'real_prefix'): + # Modified by @gcasella to use the function created on lines in __init__.py. + if venv_check(): mock_is_file.side_effect = lambda arg: arg in [os.path.join(sys.prefix, 'etc', 'jsnapy', 'jsnapy.cfg')] diff --git a/tests/unit/test_jsnapy.py b/tests/unit/test_jsnapy.py index ce93e15d..fded6857 100644 --- a/tests/unit/test_jsnapy.py +++ b/tests/unit/test_jsnapy.py @@ -47,8 +47,9 @@ def test_snap(self, mock_parse): js.main_file) self.assertTrue(mock_parse.called) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.SnapAdmin.connect') - def test_hostname(self, mock_connect): + def test_hostname(self, mock_connect, mock_extract_test_cases): argparse.ArgumentParser.parse_args = MagicMock() argparse.ArgumentParser.parse_args.return_value = argparse.Namespace(check=False, diff=False, file=None, hostname=None, @@ -56,25 +57,28 @@ def test_hostname(self, mock_connect): post_snapfile=None, pre_snapfile=None, snap=False, snapcheck=False, verbosity=None, version=False) + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() conf_file = os.path.join(os.path.dirname(__file__), 'configs', 'main_6.yml') config_file = open(conf_file, 'r') js.main_file = yaml.load(config_file, Loader=yaml.FullLoader) js.login("snap_1") - expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1'), - call('1.1.1.15', 'abc', 'xyz', 'snap_1'), - call('1.1.1.16', 'abc', 'xyz', 'snap_1'), + expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1', "tests"), + call('1.1.1.15', 'abc', 'xyz', 'snap_1', "tests"), + call('1.1.1.16', 'abc', 'xyz', 'snap_1', "tests"), ] hosts = ['1.1.1.1', '1.1.1.15', '1.1.1.16'] self.assertEqual(js.host_list, hosts) mock_connect.assert_has_calls(expected_calls_made, any_order=True) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('argparse.ArgumentParser.exit') @patch('jnpr.jsnapy.SnapAdmin.connect') @patch('jnpr.jsnapy.jsnapy.get_path') - def test_multiple_hostname(self, mock_path, mock_connect, mock_arg): + def test_multiple_hostname(self, mock_path, mock_connect, mock_arg, + mock_extract_test_cases): argparse.ArgumentParser.parse_args = MagicMock() argparse.ArgumentParser.parse_args.return_value = argparse.Namespace(check=False, diff=False, file=None, hostname=None, @@ -83,6 +87,7 @@ def test_multiple_hostname(self, mock_path, mock_connect, mock_arg): snap=False, snapcheck=False, verbosity=None, version=False) mock_path.return_value = os.path.join(os.path.dirname(__file__), 'configs') + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() conf_file = os.path.join(os.path.dirname(__file__), 'configs', 'main1.yml') @@ -94,11 +99,11 @@ def test_multiple_hostname(self, mock_path, mock_connect, mock_arg): # extending the test to check for device overlap among groups js.main_file['hosts'][0]['group'] = 'MX, EX' - expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1'), - call('1.1.1.4', 'abc', 'def', 'snap_1'), - call('1.1.1.5', 'abc', 'def', 'snap_1'), - call('1.1.1.6', 'abc', 'def', 'snap_1'), - call('1.1.1.12', 'abc', 'def', 'snap_1'), + expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1', "tests"), + call('1.1.1.4', 'abc', 'def', 'snap_1', "tests"), + call('1.1.1.5', 'abc', 'def', 'snap_1', "tests"), + call('1.1.1.6', 'abc', 'def', 'snap_1', "tests"), + call('1.1.1.12', 'abc', 'def', 'snap_1', "tests"), ] js.login("snap_1") @@ -126,8 +131,9 @@ def test_connect_snap(self, mock_log, mock_gen_reply, mock_dev, mock_arg): self.assertTrue(mock_gen_reply.called) self.assertTrue(mock_dev.called) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('logging.Logger.error') # new - def test_hostname_keyError(self, mock_log): + def test_hostname_keyError(self, mock_log, mock_extract_test_cases): js = SnapAdmin() conf_file = os.path.join(os.path.dirname(__file__), 'configs', 'main_false_keyError.yml') @@ -150,8 +156,9 @@ def test_hostname_keyError_general(self, mock_log): self.assertTrue(mock_log.called) mock_log.assert_called() + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('logging.Logger.error') # new - def test_hostname_keyError_device(self, mock_log): + def test_hostname_keyError_device(self, mock_log, mock_extract_test_cases): js = SnapAdmin() conf_file = os.path.join(os.path.dirname(__file__), 'configs', 'main_false_keyError_device.yml') @@ -170,9 +177,11 @@ def test_multiple_hostname_1(self, mock_connect): hosts = ['1.1.1.3', '1.1.1.4', '1.1.1.5'] self.assertEqual(js.host_list, hosts) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.SnapAdmin.connect') # new - def test_hostname_hostname_argument(self, mock_connect): + def test_hostname_hostname_argument(self, mock_connect, mock_extract_test_cases): js = SnapAdmin() + js.main_file = "tests" js.args.hostname = '1.1.1.3' js.args.login = 'abc' js.args.passwd = 'xyz' @@ -483,7 +492,7 @@ def test_check_arguments_9(self, mock_sys, mock_log, mock_dev): config_file = open(js.args.file, 'r') config_data = yaml.load(config_file, Loader=yaml.FullLoader) with patch(builtin_string + 'input', return_value='abc'): - js.connect('1.1.1.1', None, 'xyz', 'snap_1', config_data) + js.connect('1.1.1.1', None, 'xyz', 'snap_1', None, config_data) # username='abc' mock_log.assert_called() mock_dev.assert_called_with(host='1.1.1.1', user='abc', passwd='xyz', gather_facts=False) @@ -499,7 +508,7 @@ def test_check_arguments_10(self, mock_log, mock_dev): 'configs', 'main_1.yml') config_file = open(js.args.file, 'r') config_data = yaml.load(config_file, Loader=yaml.FullLoader) - js.connect('1.1.1.1', 'abc', 'xyz', 'snap_1', config_data) + js.connect('1.1.1.1', 'abc', 'xyz', 'snap_1', None, config_data) mock_log.assert_called() mock_dev.assert_called_with(host='1.1.1.1', user='abc', passwd='xyz', gather_facts=False) @@ -518,8 +527,9 @@ def test_check_arguments_11(self, mock_log, mock_dev): mock_dev.side_effect = Exception() js.connect('1.1.1.1', 'abc', None, 'snap_1', config_data) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('os.path.isfile') # new - def test_multiple_devices_1(self, mock_isfile): + def test_multiple_devices_1(self, mock_isfile, mock_extract_test_cases): mock_isfile.return_value = lambda arg: arg == 'devices.yml' js = SnapAdmin() js.args.file = os.path.join(os.path.dirname(__file__), 'configs', 'main1.yml') @@ -558,8 +568,9 @@ def test_multiple_devices_3(self): # new hosts_required = ['1.1.1.1', '1.1.1.15', '1.1.1.16'] self.assertEqual(js.host_list, hosts_required) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('logging.Logger.error') # new - def test_multiple_devices_4(self, mock_log): + def test_multiple_devices_4(self, mock_log, mock_extract_test_cases): js = SnapAdmin() js.args.file = os.path.join(os.path.dirname(__file__), 'configs', 'main_false_keyError_device.yml') config_file = open(js.args.file, 'r') @@ -595,11 +606,14 @@ def test_extract_data_1(self, mock_multiple): # new js.extract_data(js.args.file, 'mock_pre', None, 'mock_post') mock_multiple.assert_called() + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.jsnapy.setup_logging.setup_logging') @patch('os.path.isfile') @patch('jnpr.jsnapy.jsnapy.SnapAdmin.get_values') # new - def test_extract_data_2(self, mock_getvalue, mock_isfile, mock_logging): + def test_extract_data_2(self, mock_getvalue, mock_isfile, mock_logging, + mock_extract_test_cases): mock_isfile.return_value = False + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() config_data = """ hosts: @@ -632,11 +646,14 @@ def test_extract_data_4(self, mock_log, mock_isfile, mock_logging): mock_log.assert_called() mock_logging.assert_called() + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.jsnapy.setup_logging.setup_logging') @patch('os.path.isfile') # new @patch('jnpr.jsnapy.jsnapy.SnapAdmin.chk_database') - def test_extract_data_5(self, mock_chkdb, mock_isfile, mock_logging): + def test_extract_data_5(self, mock_chkdb, mock_isfile, mock_logging, + mock_extract_test_cases): mock_isfile.return_value = False + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() config_data = """ hosts: @@ -1273,9 +1290,11 @@ def test_sqlite_parameters_7(self, mock_login, mock_arg): js.get_hosts() self.assertEqual(js.db, self.db) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.SnapAdmin.connect') - def test_port_without_include(self, mock_connect): + def test_port_without_include(self, mock_connect, mock_extract_test_cases): # this test case is for scenarios when devices are mentioned in the cfg file itself + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() # js.args.snap = True js.args.hostname = None @@ -1287,7 +1306,7 @@ def test_port_without_include(self, mock_connect): js.login("snap_1") hosts = ['1.1.1.1'] self.assertEqual(js.host_list, hosts) - mock_connect.assert_called_with('1.1.1.1', 'abc', 'xyz', 'snap_1', port=44) + mock_connect.assert_called_with('1.1.1.1', 'abc', 'xyz', 'snap_1', "tests", port=44) # adding another device in the config dictionary # and checking the precedence b/w cmd and config params @@ -1297,8 +1316,8 @@ def test_port_without_include(self, mock_connect): 'port': 45}) js.args.port = 100 - expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1', port=100), - call('1.1.1.15', 'abc', 'xyz', 'snap_1', port=100)] + expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1',"tests", port=100), + call('1.1.1.15', 'abc', 'xyz', 'snap_1',"tests", port=100)] js.login("snap_1") mock_connect.assert_has_calls(expected_calls_made, any_order=True) @@ -1311,16 +1330,18 @@ def test_port_without_include(self, mock_connect): mock_connect.assert_has_calls(expected_calls_made, any_order=True) # deleting the cmd line port param - expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1'), - call('1.1.1.15', 'abc', 'xyz', 'snap_1')] + expected_calls_made = [call('1.1.1.1', 'abc', 'xyz', 'snap_1', "tests"), + call('1.1.1.15', 'abc', 'xyz', 'snap_1', "tests")] js.args.port = None js.login("snap_1") mock_connect.assert_has_calls(expected_calls_made, any_order=True) + @patch('jnpr.jsnapy.SnapAdmin.extract_test_cases') @patch('jnpr.jsnapy.jsnapy.get_path') @patch('jnpr.jsnapy.SnapAdmin.connect') - def test_port_with_include(self, mock_connect, mock_path): + def test_port_with_include(self, mock_connect, mock_path, mock_extract_test_cases): # this test case is for scenarios when devices are included using some other file + mock_extract_test_cases.return_value = "tests" js = SnapAdmin() js.args.snap = True js.args.hostname = None @@ -1332,9 +1353,9 @@ def test_port_with_include(self, mock_connect, mock_path): js.main_file = yaml.load(config_file, Loader=yaml.FullLoader) hosts = ['1.1.1.3', '1.1.1.4', '1.1.1.5'] - expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1', port=100), - call('1.1.1.4', 'abc', 'def', 'snap_1', port=101), - call('1.1.1.5', 'abc', 'def', 'snap_1', port=102)] + expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1', "tests", port=100), + call('1.1.1.4', 'abc', 'def', 'snap_1', "tests", port=101), + call('1.1.1.5', 'abc', 'def', 'snap_1', "tests", port=102)] js.login("snap_1") @@ -1345,9 +1366,9 @@ def test_port_with_include(self, mock_connect, mock_path): # Adding the cmd-line port param and checking the precedence b/w cmd and config params js.args.port = 55 - expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1', port=55), - call('1.1.1.4', 'abc', 'def', 'snap_1', port=55), - call('1.1.1.5', 'abc', 'def', 'snap_1', port=55)] + expected_calls_made = [call('1.1.1.3', 'abc', 'def', 'snap_1', "tests", port=55), + call('1.1.1.4', 'abc', 'def', 'snap_1', "tests", port=55), + call('1.1.1.5', 'abc', 'def', 'snap_1', "tests", port=55)] js.login("snap_1") mock_connect.assert_has_calls(expected_calls_made, any_order=True) diff --git a/tests/unit/test_sqlite.py b/tests/unit/test_sqlite.py index 4cf2ae6b..38642720 100644 --- a/tests/unit/test_sqlite.py +++ b/tests/unit/test_sqlite.py @@ -1,5 +1,6 @@ import unittest import os +import sys from jnpr.jsnapy.sqlite_store import JsnapSqlite from jnpr.jsnapy.sqlite_get import SqliteExtractXml from mock import patch @@ -80,7 +81,6 @@ def fun(): def test_sqlite_3(self, mock_spath, mock_path, mock_sys): mock_path.return_value = os.path.join(os.path.dirname(__file__), 'configs') mock_spath.return_value = os.path.join(os.path.dirname(__file__), 'configs') - js = JsnapSqlite("1.1.1.1", self.db) js.insert_data(self.db_dict2) with patch('logging.Logger.error') as mock_log: @@ -90,8 +90,11 @@ def test_sqlite_3(self, mock_spath, mock_path, mock_sys): self.assertEqual(data, "mock_data") self.assertEqual(formt, "text") extr.get_xml_using_snap_id("1.1.1.1", "show vers", 0) - err = [ - "ERROR!! Complete message is: 'NoneType' object is not iterable"] + if sys.version_info[0] == 2 or (sys.version_info[0] == 3 and sys.version_info[1] <= 6): + err = ["ERROR!! Complete message is: 'NoneType' object is not iterable"] + else : + err = ["ERROR!! Complete message is: cannot unpack non-iterable NoneType object"] + c_list = mock_log.call_args_list[0] self.assertNotEqual(c_list[0][0].find(err[0]), -1)