def __init__(self, message: Message, common_parser_args: CommonParserArguments): self.issues_handler = IssueHandler() self._common_parser_args = common_parser_args self._message = message self.device_id = "" # need to default self.device_id = self._parse_device_id(message) self.interface_name = self._parse_interface_name(message) self.module_id = self._parse_module_id(message)
def _validate_payload_against_entities(self, payload: dict, name, minimum_severity): name_miss = [] issues_handler = IssueHandler() if not self._is_component(payload): # update is not part of a component check under interfaces schema = self._template.get_schema(name=name) if not schema: name_miss.append(name) details = strings.invalid_field_name_mismatch_template( name_miss, self._template.schema_names) interfaces_with_specified_property = self._template._get_interface_list_property( name) if len(interfaces_with_specified_property) > 1: details = strings.duplicate_property_name( name, interfaces_with_specified_property) issues_handler.add_central_issue( severity=Severity.warning, details=details, message=None, device_id=self._device_id, template_id=self._template.id, ) else: # Property update is part of a component perform additional validations under component list. component_property_updates = [ property_name for property_name in payload if property_name != PNP_DTDLV2_COMPONENT_MARKER ] for property_name in component_property_updates: schema = self._template.get_schema(name=property_name, identifier=name, is_component=True) if not schema: name_miss.append(property_name) details = strings.invalid_field_name_component_mismatch_template( name_miss, self._template.component_schema_names) if name_miss: issues_handler.add_central_issue( severity=Severity.warning, details=details, message=None, device_id=self._device_id, template_id=self._template.id, ) return issues_handler.get_issues_with_minimum_severity( minimum_severity)
def _validate_payload_against_interfaces(self, payload: dict, name, minimum_severity): name_miss = [] issues_handler = IssueHandler() interface_name = name.replace(CENTRAL_PNP_INTERFACE_PREFIX, "") if self._is_interface(interface_name): # if the payload is an interface then iterate thru the properties under the interface for property_name in payload: schema = self._template.get_schema( name=property_name, interface_name=interface_name) if not schema: name_miss.append(property_name) else: # if the payload is a property then process the payload as a single unit. schema = self._template.get_schema(name=name) if not schema: name_miss.append(name) interfaces_with_specified_property = self._template._get_interface_list_property( name) if len(interfaces_with_specified_property) > 1: details = strings.duplicate_property_name( name, interfaces_with_specified_property) issues_handler.add_central_issue( severity=Severity.warning, details=details, message=None, device_id=self._device_id, template_id=self._template.id, ) if name_miss: details = strings.invalid_field_name_mismatch_template( name_miss, self._template.schema_names) issues_handler.add_central_issue( severity=Severity.warning, details=details, message=None, device_id=self._device_id, template_id=self._template.id, ) return issues_handler.get_issues_with_minimum_severity( minimum_severity)
class CommonParser(AbstractBaseParser): def __init__(self, message: Message, common_parser_args: CommonParserArguments): self.issues_handler = IssueHandler() self._common_parser_args = common_parser_args self._message = message self.device_id = "" # need to default self.device_id = self._parse_device_id(message) self.module_id = self._parse_module_id(message) self.interface_name = self._parse_interface_name(message) self.component_name = self._parse_component_name(message) def parse_message(self) -> dict: """ Parses an AMQP based IoT Hub telemetry event. """ message = self._message properties = self._common_parser_args.properties content_type = self._common_parser_args.content_type event = {} event["origin"] = self.device_id event["module"] = self.module_id event["interface"] = self.interface_name event["component"] = self.component_name if not properties: properties = [] # guard against None being passed in system_properties = self._parse_system_properties(message) self._parse_content_encoding(message, system_properties) content_type = self._parse_content_type(content_type, system_properties) if properties: event["properties"] = {} if "anno" in properties or "all" in properties: annotations = self._parse_annotations(message) event["annotations"] = annotations if system_properties and ("sys" in properties or "all" in properties): event["properties"]["system"] = system_properties if "app" in properties or "all" in properties: application_properties = self._parse_application_properties( message) event["properties"]["application"] = application_properties payload = self._parse_payload(message, content_type) event["payload"] = payload event_source = {"event": event} return event_source def _add_issue(self, severity: Severity, details: str): self.issues_handler.add_issue( severity=severity, details=details, message=self._message, device_id=self.device_id, ) def _parse_device_id(self, message: Message) -> str: try: return str(message.annotations.get(DEVICE_ID_IDENTIFIER), "utf8") except Exception: details = strings.unknown_device_id() self._add_issue(severity=Severity.error, details=details) return "" def _parse_module_id(self, message: Message) -> str: try: return str(message.annotations.get(MODULE_ID_IDENTIFIER), "utf8") except Exception: # a message not containing an module name is expected for non-edge devices # so there's no "issue" to log here return "" def _parse_interface_name(self, message: Message) -> str: try: # Grab either the DTDL v1 or v2 amqp interface identifier. # It's highly unlikely both will be present at the same time # as they reflect different versions of a Plug & Play device. target_interface = message.annotations.get( INTERFACE_NAME_IDENTIFIER_V1) or message.annotations.get( INTERFACE_NAME_IDENTIFIER_V2) return str(target_interface, "utf8") except Exception: # a message not containing an interface name is expected for non-pnp devices # so there's no "issue" to log here return "" def _parse_component_name(self, message: Message) -> str: try: return str(message.annotations.get(COMPONENT_NAME_IDENTIFIER), "utf8") except Exception: return "" def _parse_system_properties(self, message: Message): try: return unicode_binary_map(parse_entity(message.properties, True)) except Exception: details = strings.invalid_system_properties() self._add_issue(severity=Severity.warning, details=details) return {} def _parse_content_encoding(self, message: Message, system_properties) -> str: content_encoding = "" if "content_encoding" in system_properties: content_encoding = system_properties["content_encoding"] if not content_encoding: details = strings.invalid_encoding_none_found() self._add_issue(severity=Severity.warning, details=details) return None if "utf-8" not in content_encoding.lower(): details = strings.invalid_encoding(content_encoding.lower()) self._add_issue(severity=Severity.warning, details=details) return None return content_encoding def _parse_content_type(self, expected_content_type: str, system_properties: dict) -> str: actual_content_type = system_properties.get("content_type", "") # Device data is not expected to be of a certain type # Continue parsing per rules that the device is sending if not expected_content_type: return actual_content_type.lower() # Device is expected to send data in a certain format. # Data from device implies the data is in an incorrect format. # Log the issue, and continue parsing as if device is in expected format. if actual_content_type.lower() != expected_content_type.lower(): details = strings.content_type_mismatch(actual_content_type, expected_content_type) self._add_issue(severity=Severity.warning, details=details) return expected_content_type.lower() return actual_content_type def _parse_annotations(self, message: Message): try: return unicode_binary_map(message.annotations) except Exception: details = strings.invalid_annotations() self._add_issue(severity=Severity.warning, details=details) return {} def _parse_application_properties(self, message: Message): try: return unicode_binary_map(message.application_properties) except Exception: details = strings.invalid_application_properties() self._add_issue(severity=Severity.warning, details=details) return {} def _parse_payload(self, message: Message, content_type): payload = "" data = message.get_data() if data: payload = str(next(data), "utf8") if "application/json" in content_type.lower(): return self._try_parse_json(payload) return payload def _try_parse_json(self, payload): result = payload try: regex = r"(\\r\\n)+|\\r+|\\n+" payload_no_white_space = re.compile(regex).sub("", payload) result = json.loads(payload_no_white_space) except Exception: details = strings.invalid_json() self._add_issue(severity=Severity.error, details=details) return result