def test_response_init(response_setup): netconf_version = response_setup[0] channel_input = response_setup[1] xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=netconf_version, failed_when_contains=[b"<rpc-error>"], ) response_start_time = str(datetime.now())[:-7] assert response.host == "localhost" assert response.channel_input == "<something/>" assert response.xml_input == xml_input assert str(response.start_time)[:-7] == response_start_time assert response.failed is True assert bool(response) is True assert ( repr(response) == "NetconfResponse(host='localhost',channel_input='<something/>',textfsm_platform='',genie_platform='',failed_when_contains=[b'<rpc-error>'])" ) assert str(response) == "NetconfResponse <Success: False>" assert response.failed_when_contains == [b"<rpc-error>"] with pytest.raises(ScrapliCommandFailure): response.raise_for_status()
def test_failed_when_contains_default_values(response_data): response_output = response_data[0] response_success = response_data[1] channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=NetconfVersion.VERSION_1_0, ) response.record_response(result=response_output) assert response.failed is not response_success
def test_parse_error_messages(response_data): response_output = response_data[0] expected_errors = response_data[1] channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=NetconfVersion.VERSION_1_1, ) response.record_response(result=response_output.encode()) assert response.error_messages == expected_errors
def _pre_discard(self) -> NetconfResponse: """ Handle pre "discard" tasks for consistency between sync/async versions Args: N/A Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for 'discard' operation.") xml_request = self._build_base_elem() xml_commit_element = etree.fromstring( NetconfBaseOperations.DISCARD.value, parser=self.xml_parser) xml_request.insert(0, xml_commit_element) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'discard' operation. Payload: {channel_input.decode()}" ) return response
def test__validate_chunk_size_netconf_1_1(response_data): chunk_input = response_data[0] response_success = response_data[1] channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=NetconfVersion.VERSION_1_1, failed_when_contains=[b"<rpc-error>"], ) # set response.failed because we are skipping "record_response" response.failed = False response._validate_chunk_size_netconf_1_1(result=chunk_input) assert response.failed is not response_success
def _pre_edit_config(self, config: Union[str, List[str]], target: str = "running") -> NetconfResponse: """ Handle pre "edit_config" tasks for consistency between sync/async versions Args: config: configuration to send to device target: configuration source to target; running|startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for `get-config` operation. target: {target}, config: {config}" ) self._validate_edit_config_target(target=target) # build config first to ensure valid xml xml_config = etree.fromstring(config) # build base request and insert the edit-config element xml_request = self._build_base_elem() xml_edit_config_element = etree.fromstring( NetconfBaseOperations.EDIT_CONFIG.value.format(target=target)) xml_request.insert(0, xml_edit_config_element) # insert parent filter element to first position so that target stays first just for nice # output/readability edit_config_element = xml_request.find("edit-config") edit_config_element.insert(1, xml_config) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.transport.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for `edit-config` operation. Payload: {channel_input.decode()}" ) return response
def _pre_get(self, filter_: str, filter_type: str = "subtree") -> NetconfResponse: """ Handle pre "get" tasks for consistency between sync/async versions Args: filter_: string filter to apply to the get filter_type: type of filter; subtree|xpath Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for `get` operation. filter_type: {filter_type}, filter_: {filter_}" ) # build base request and insert the get element xml_request = self._build_base_elem() xml_get_element = etree.fromstring(NetconfBaseOperations.GET.value) xml_request.insert(0, xml_get_element) xml_filter_elem = self._build_filters(filters=[filter_], filter_type=filter_type) # insert filter element into parent get element get_element = xml_request.find("get") get_element.insert(0, xml_filter_elem) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.transport.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for `get` operation. Payload: {channel_input.decode()}" ) return response
def test_record_response(response_setup): netconf_version = response_setup[0] strip_namespaces = response_setup[1] channel_input = response_setup[2] result = response_setup[3] final_result = response_setup[4] xml_elements = response_setup[5] xml_input = etree.fromstring(text=channel_input) response_end_time = str(datetime.now())[:-7] response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=netconf_version, failed_when_contains=[b"<rpc-error>"], strip_namespaces=strip_namespaces, ) response.record_response(result=result.encode()) assert str(response.finish_time)[:-7] == response_end_time assert response.result == final_result assert response.failed is False assert list(response.get_xml_elements().keys()) == xml_elements
def test_response_init_exception(): netconf_version = "blah" channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) with pytest.raises(ValueError) as exc: NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=netconf_version, failed_when_contains=[b"<rpc-error>"], ) assert str(exc.value) == "`netconf_version` should be one of 1.0|1.1, got `blah`"
def test_response_not_implemented_exceptions(method_to_test): channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=NetconfVersion.VERSION_1_0, failed_when_contains=[b"<rpc-error>"], ) method = getattr(response, f"{method_to_test}_parse_output") with pytest.raises(NotImplementedError) as exc: method() assert str(exc.value) == f"No {method_to_test} parsing for netconf output!"
def _pre_validate(self, source: str) -> NetconfResponse: """ Handle pre "validate" tasks for consistency between sync/async versions Args: source: configuration source to validate; typically one of running|startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: CapabilityNotSupported: if 'validate' capability does not exist """ self.logger.debug("Building payload for 'validate' operation.") if not any(cap in self.server_capabilities for cap in ( "urn:ietf:params:netconf:capability:validate:1.0", "urn:ietf:params:netconf:capability:validate:1.1", )): msg = "validate requested, but is not supported by the server" self.logger.exception(msg) raise CapabilityNotSupported(msg) self._validate_edit_config_target(target=source) xml_request = self._build_base_elem() xml_validate_element = etree.fromstring( NetconfBaseOperations.VALIDATE.value.format(source=source), parser=PARSER) xml_request.insert(0, xml_validate_element) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'validate' operation. Payload: {channel_input.decode()}" ) return response
def _pre_edit_config(self, config: str, target: str = "running") -> NetconfResponse: """ Handle pre "edit_config" tasks for consistency between sync/async versions Args: config: configuration to send to device target: configuration source to target; running|startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for 'edit-config' operation. target: {target}, config: {config}" ) self._validate_edit_config_target(target=target) xml_config = etree.fromstring(config, parser=self.xml_parser) # build base request and insert the edit-config element xml_request = self._build_base_elem() xml_edit_config_element = etree.fromstring( NetconfBaseOperations.EDIT_CONFIG.value.format(target=target)) xml_request.insert(0, xml_edit_config_element) # insert parent filter element to first position so that target stays first just for nice # output/readability edit_config_element = xml_request.find("edit-config") edit_config_element.insert(1, xml_config) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'edit-config' operation. Payload: {channel_input.decode()}" ) return response
def _pre_copy_config(self, source: str, target: str) -> NetconfResponse: """ Handle pre "copy_config" tasks for consistency between sync/async versions Note that source is not validated/checked since it could be a url or a full configuration element itself. Args: source: configuration, url, or datastore to copy into the target datastore target: copy config destination/target; typically one of running|startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for 'copy_config' operation.") self._validate_edit_config_target(target=target) xml_request = self._build_base_elem() xml_validate_element = etree.fromstring( NetconfBaseOperations.COPY_CONFIG.value.format(source=source, target=target), parser=self.xml_parser, ) xml_request.insert(0, xml_validate_element) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'copy-config' operation. Payload: {channel_input.decode()}" ) return response
def _pre_delete_config(self, target: str = "running") -> NetconfResponse: """ Handle pre "edit_config" tasks for consistency between sync/async versions Args: target: configuration source to target; startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for 'delete-config' operation. target: {target}" ) self._validate_delete_config_target(target=target) xml_request = self._build_base_elem() xml_validate_element = etree.fromstring( NetconfBaseOperations.DELETE_CONFIG.value.format(target=target), parser=PARSER) xml_request.insert(0, xml_validate_element) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'delete-config' operation. Payload: {channel_input.decode()}" ) return response
def _pre_rpc(self, filter_: str) -> NetconfResponse: """ Handle pre "rpc" tasks for consistency between sync/async versions Args: filter_: filter/rpc to execute Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for `rpc` operation.") xml_request = self._build_base_elem() # build filter element xml_filter_elem = etree.fromstring(filter_) # insert filter element xml_request.insert(0, xml_filter_elem) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.transport.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for `rpc` operation. Payload: {channel_input.decode()}" ) return response
def _pre_rpc(self, filter_: Union[str, _Element]) -> NetconfResponse: """ Handle pre "rpc" tasks for consistency between sync/async versions Args: filter_: filter/rpc to execute Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for 'rpc' operation.") xml_request = self._build_base_elem() # build filter element if isinstance(filter_, str): xml_filter_elem = etree.fromstring(filter_, parser=self.xml_parser) else: xml_filter_elem = filter_ # insert filter element xml_request.insert(0, xml_filter_elem) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'rpc' operation. Payload: {channel_input.decode()}" ) return response
def _pre_discard(self) -> NetconfResponse: """ Handle pre "discard" tasks for consistency between sync/async versions Args: N/A Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for `discard` operation.") xml_request = self._build_base_elem() xml_commit_element = etree.fromstring( NetconfBaseOperations.DISCARD.value) xml_request.insert(0, xml_commit_element) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.transport.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for `discard` operation. Payload: {channel_input.decode()}" ) return response
def _pre_unlock(self, target: str) -> NetconfResponse: """ Handle pre "unlock" tasks for consistency between sync/async versions Args: target: configuration source to target; running|startup|candidate Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for 'unlock' operation.") self._validate_edit_config_target(target=target) xml_request = self._build_base_elem() xml_lock_element = etree.fromstring( NetconfBaseOperations.UNLOCK.value.format(target=target, parser=self.xml_parser)) xml_request.insert(0, xml_lock_element) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'unlock' operation. Payload: {channel_input.decode()}" ) return response
def _pre_commit(self) -> NetconfResponse: """ Handle pre "commit" tasks for consistency between sync/async versions Args: N/A Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug("Building payload for 'commit' operation") xml_request = self._build_base_elem() xml_commit_element = etree.fromstring( NetconfBaseOperations.COMMIT.value, parser=PARSER) xml_request.insert(0, xml_commit_element) channel_input = etree.tostring(xml_request) if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'commit' operation. Payload: {channel_input.decode()}" ) return response
def test_raise_for_status(response_data): response_output = response_data[0] expected_errors = response_data[1] channel_input = "<something/>" xml_input = etree.fromstring(text=channel_input) response = NetconfResponse( host="localhost", channel_input=channel_input, xml_input=xml_input, netconf_version=NetconfVersion.VERSION_1_1, ) response.record_response(result=response_output.encode()) if not expected_errors: assert not response.error_messages return with pytest.raises(ScrapliCommandFailure) as exc: response.raise_for_status() assert str(exc.value ) == f"operation failed, reported rpc errors: {expected_errors}"
def _pre_get_config( self, source: str = "running", filters: Optional[Union[str, List[str]]] = None, filter_type: str = "subtree", ) -> NetconfResponse: """ Handle pre "get_config" tasks for consistency between sync/async versions Args: source: configuration source to get; typically one of running|startup|candidate filters: string or list of strings of filters to apply to configuration filter_type: type of filter; subtree|xpath Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for `get-config` operation. source: {source}, filter_type: " f"{filter_type}, filters: {filters}") self._validate_get_config_target(source=source) # build base request and insert the get-config element xml_request = self._build_base_elem() xml_get_config_element = etree.fromstring( NetconfBaseOperations.GET_CONFIG.value.format(source=source)) xml_request.insert(0, xml_get_config_element) if filters is not None: if isinstance(filters, str): filters = [filters] xml_filter_elem = self._build_filters(filters=filters, filter_type=filter_type) # insert filter element into parent get element get_element = xml_request.find("get-config") # insert *after* source, otherwise juniper seems to gripe, maybe/probably others as well get_element.insert(1, xml_filter_elem) channel_input = etree.tostring(element_or_tree=xml_request, xml_declaration=True, encoding="utf-8") if self.netconf_version == NetconfVersion.VERSION_1_0: channel_input = channel_input + b"\n]]>]]>" response = NetconfResponse( host=self.transport.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for `get-config` operation. Payload: {channel_input.decode()}" ) return response
def _pre_commit( self, confirmed: bool = False, timeout: Optional[int] = None, persist: Optional[Union[int, str]] = None, persist_id: Optional[Union[int, str]] = None, ) -> NetconfResponse: """ Handle pre "commit" tasks for consistency between sync/async versions Args: confirmed: whether this is a confirmed commit timeout: specifies the confirm timeout in seconds persist: make the confirmed commit survive a session termination, and set a token on the ongoing confirmed commit persist_id: value must be equal to the value given in the <persist> parameter to the original <commit> operation. Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: ScrapliValueError: if persist and persist_id are provided (cannot combine) ScrapliValueError: if confirmed and persist_id are provided (cannot combine) CapabilityNotSupported: if device does not have confirmed-commit capability """ self.logger.debug("Building payload for 'commit' operation") xml_request = self._build_base_elem() xml_commit_element = etree.fromstring( NetconfBaseOperations.COMMIT.value, parser=self.xml_parser) if persist and persist_id: raise ScrapliValueError( "Invalid combination - 'persist' cannot be present with 'persist-id'" ) if confirmed and persist_id: raise ScrapliValueError( "Invalid combination - 'confirmed' cannot be present with 'persist-id'" ) if confirmed or persist_id: if not any(cap in self.server_capabilities for cap in ( "urn:ietf:params:netconf:capability:confirmed-commit:1.0", "urn:ietf:params:netconf:capability:confirmed-commit:1.1", )): msg = "confirmed-commit requested, but is not supported by the server" self.logger.exception(msg) raise CapabilityNotSupported(msg) if confirmed: xml_confirmed_element = etree.fromstring( NetconfBaseOperations.COMMIT_CONFIRMED.value, parser=self.xml_parser) xml_commit_element.append(xml_confirmed_element) if timeout is not None: xml_timeout_element = etree.fromstring( NetconfBaseOperations.COMMIT_CONFIRMED_TIMEOUT.value. format(timeout=timeout), parser=self.xml_parser, ) xml_commit_element.append(xml_timeout_element) if persist is not None: xml_persist_element = etree.fromstring( NetconfBaseOperations.COMMIT_CONFIRMED_PERSIST.value. format(persist=persist), parser=self.xml_parser, ) xml_commit_element.append(xml_persist_element) if persist_id is not None: xml_persist_id_element = etree.fromstring( NetconfBaseOperations.COMMIT_PERSIST_ID.value.format( persist_id=persist_id), parser=self.xml_parser, ) xml_commit_element.append(xml_persist_id_element) xml_request.insert(0, xml_commit_element) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'commit' operation. Payload: {channel_input.decode()}" ) return response
def _pre_get_config( self, source: str = "running", filter_: Optional[str] = None, filter_type: str = "subtree", default_type: Optional[str] = None, ) -> NetconfResponse: """ Handle pre "get_config" tasks for consistency between sync/async versions Args: source: configuration source to get; typically one of running|startup|candidate filter_: string of filter(s) to apply to configuration filter_type: type of filter; subtree|xpath default_type: string of with-default mode to apply when retrieving configuration Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for 'get-config' operation. source: {source}, filter_type: " f"{filter_type}, filter: {filter_}, default_type: {default_type}") self._validate_get_config_target(source=source) # build base request and insert the get-config element xml_request = self._build_base_elem() xml_get_config_element = etree.fromstring( NetconfBaseOperations.GET_CONFIG.value.format(source=source), parser=self.xml_parser) xml_request.insert(0, xml_get_config_element) if filter_ is not None: xml_filter_elem = self._build_filter(filter_=filter_, filter_type=filter_type) # insert filter element into parent get element get_element = xml_request.find("get-config") # insert *after* source, otherwise juniper seems to gripe, maybe/probably others as well get_element.insert(1, xml_filter_elem) if default_type is not None: xml_with_defaults_elem = self._build_with_defaults( default_type=default_type) get_element = xml_request.find("get-config") get_element.insert(2, xml_with_defaults_elem) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'get-config' operation. Payload: {channel_input.decode()}" ) return response
def _pre_get(self, filter_: str, filter_type: str = "subtree") -> NetconfResponse: """ Handle pre "get" tasks for consistency between sync/async versions *NOTE* The channel input (filter_) is loaded up as an lxml etree element here, this is done with a parser that removes whitespace. This has a somewhat undesirable effect of making any "pretty" input not pretty, however... after we load the xml object (which we do to validate that it is valid xml) we dump that xml object back to a string to be used as the actual raw payload we send down the channel, which means we are sending "flattened" (not pretty/ indented xml) to the device. This is important it seems! Some devices seme to not mind having the "nicely" formatted input (pretty xml). But! On devices that "echo" the inputs back -- sometimes the device will respond to our rpc without "finishing" echoing our inputs to the device, this breaks the core "read until input" processing that scrapli always does. For whatever reason if there are no line breaks this does not seem to happen? /shrug. Note that this comment applies to all of the "pre" methods that we parse a filter/payload! Args: filter_: string filter to apply to the get filter_type: type of filter; subtree|xpath Returns: NetconfResponse: scrapli_netconf NetconfResponse object containing all the necessary channel inputs (string and xml) Raises: N/A """ self.logger.debug( f"Building payload for 'get' operation. filter_type: {filter_type}, filter_: {filter_}" ) # build base request and insert the get element xml_request = self._build_base_elem() xml_get_element = etree.fromstring(NetconfBaseOperations.GET.value) xml_request.insert(0, xml_get_element) # build filter element xml_filter_elem = self._build_filter(filter_=filter_, filter_type=filter_type) # insert filter element into parent get element get_element = xml_request.find("get") get_element.insert(0, xml_filter_elem) channel_input = self._finalize_channel_input(xml_request=xml_request) response = NetconfResponse( host=self.host, channel_input=channel_input.decode(), xml_input=xml_request, netconf_version=self.netconf_version, strip_namespaces=self.strip_namespaces, ) self.logger.debug( f"Built payload for 'get' operation. Payload: {channel_input.decode()}" ) return response