Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint redirection documentation #447

Merged
merged 20 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _includes/head-custom.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("pre>code.language-mermaid").forEach($element => {
$element.parentElement.outerHTML = `<div class="mermaid">${$element.textContent}</div>`
$element.parentElement.outerHTML = `<div class="mermaid">${$element.innerHTML}</div>`
});
mermaid.initialize();
});
Expand Down
7 changes: 6 additions & 1 deletion bin/gencode
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ if [[ $1 == check ]]; then
shift
fi

if [[ -n $check ]]; then
# This check must be run before gencode_docs is run
bin/gencode_docs_examples check
fi

bin/gencode_java
bin/gencode_python gencode/python schema/*.json
bin/gencode_docs
bin/gencode_seq

if [[ -n $check ]]; then
echo Checking gencode docs links...
bin/gencode_docs check_links
bin/gencode_docs_checklinks

echo "Checking gencode hash (dynamic/static)..."
files=`find gencode/ -type f | sort`
Expand Down
File renamed without changes.
143 changes: 143 additions & 0 deletions bin/gencode_docs_examples
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
""" Inline update documents with JSON payloads from tests directory"""

import glob
import re
import sys
import argparse

from datetime import datetime


REGEX_NORMALIZE = r'(?s)(<!--example:\w+\/\w+.json-->\n)(```json.*?```)'
REGEX_INCLUDE = r'<!--example:(\w+)\/(\w+).json-->\n'


def parse_command_line_args():
parser = argparse.ArgumentParser()

parser.add_argument('check', nargs='?', default=False,
help='Check in-line examples match examples in tests directory')

return parser.parse_args()


def validate_code_blocks(file_contents):
""" Run some very primitive checks to validate templated placeholder code
grafnu marked this conversation as resolved.
Show resolved Hide resolved
blocks within file to prevent unwanted deletion of parts of documents. Works
by checking the code blocks for lines which start unlike a JSON line (caused
by mismatched ``` which result in the regex match extending into the of the
document) or the letter F(ile not found)

Arguments
file_contents contents of file
Returns
true/false if file is valid
"""
matches = re.findall(REGEX_NORMALIZE, file_contents)
for match in matches:
code_block_removed = re.sub(r'(?s)```json(.*)```', r'\g<1>', match[1])
if re.search(r'(?m)^[^{}"\sF\/]', code_block_removed):
return False
return True


def read_example_file(match):
"""
Used as re.sub callback to read example from a file
Arguments
match match object
Returns
json from example file appended to original expression
"""
expression = match.group(0)
schema = match.group(1)
file = match.group(2)

include_path = f'tests/{schema}.tests/{file}.json'

try:
with open(include_path, 'r', encoding='utf-8') as f:
return f'{expression}```json\n{f.read().rstrip()}\n```'
except FileNotFoundError:
# Append time for not found errors so there's always a diff
return f'{expression}```json\nFILE NOT FOUND ({datetime.now()})\n```'


def include_examples(file_contents):
""" Replaces examples within a string
Arguments
file_contents string to replace in (contents of documentation file)
Returns
string contents of file
"""
normalized_file = re.sub(REGEX_NORMALIZE, r'\g<1>', file_contents)
return re.sub(REGEX_INCLUDE, read_example_file, normalized_file)


def diff_examples(original, updated):
""" Compares documentation before and after external sources are updated
and returns a list of JSON code blocks which are different between the two
Arguments:
original original documentation file contents
updated updated (in memory) documentation file contents
Returns:
list list of expressions (<!--example:metadata/tutorial_hvac_min.json-->)
which differ
"""
diffs = []

matches_original = re.findall(REGEX_NORMALIZE, original)
matches_updated = re.findall(REGEX_NORMALIZE, updated)

for match_original, match_updated in zip(matches_original, matches_updated):
if match_original[1] != match_updated[1]:
diffs.append(match_original[0].rstrip())

return diffs


def main():
file_paths = glob.glob('docs/**/*.md', recursive=True)

args = parse_command_line_args()
example_diffs = {}
error = False

for file_path in file_paths:

with open(file_path, 'r', encoding='utf-8') as f:
file_contents = f.read()

if not validate_code_blocks(file_contents):
error = True
print(f'Error processing {file_path}:')
print('\tMismatched code block termination (```) ',
'or invalid JSON in codeblock')
continue

updated_file_contents = include_examples(file_contents)

if updated_file_contents != file_contents:
if args.check:
example_diffs[file_path] = diff_examples(file_contents,
updated_file_contents)
else:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_file_contents)
if args.check:
if example_diffs:
error = True
print('Examples do not match source in following files:')
for file, diffs in example_diffs.items():
print(f'{file}')
for diff in diffs:
print(f'** {diff}')
else:
print('Documentation in line examples match source examples')
if error:
sys.exit(1)


if __name__ == '__main__':
main()
142 changes: 142 additions & 0 deletions docs/specs/sequences/endpoint_reconfiguration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
[**UDMI**](../../../) / [**Docs**](../../) / [**Specs**](../) / [**Sequences**](./) / [Endpoint Reconfiguration](#)

# Endpoint Reconfiguration

Endpoint reconfiguration is the reconfiguration of the UDMI endpoint through the
UDMI blob delivery mechanisms via UDMI config messages.

The [endpoint configuration blob](https://github.com/faucetsdn/udmi/blob/master/tests/configuration_endpoint.tests/simple.json) is a JSON object defined by
[configuration_endpoint.json](https://faucetsdn.github.io/udmi/gencode/docs/configuration_endpoint.html), which is base64 encoded in the config message.


## Tests

### Valid Endpoint (Successful) Reconfiguration

Note - `<ENDPOINT>` is a **base64** encoded endpoint object

```mermaid
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a place we can go to see a rendered version of this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%%{wrap}%%
sequenceDiagram
autonumber
participant D as Device
participant E as Original Endpoint
participant E' as New Endpoint
E->>D:CONFIG MESSAGE<br>blobset.blobs._iot_endpoint_config.base64 = <ENDPOINT><br>blobset.blobs._iot_endpoint.phase = "final"
noursaidi marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pedantically there should be a check at the start that verifies that state.blobset.blobs._iot_endpoint_config == null (as the config block is empty so there should be no corresponding state).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

D->>E:STATE MESSAGE<BR>blobset.blobs._iot_endpoint_config.phase = "apply"
D-->>E':CONNECTION ATTEMPT
grafnu marked this conversation as resolved.
Show resolved Hide resolved
E'->>D:CONFIG MESSAGE
D->>E':STATE MESSAGE<BR>blobset.blobs._iot_endpoint_config.phase = "final"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add an additional config/state sequence at the end that starts with config.blobset.blobs._iot_endpoint_config = null and then we verify that the state.blobset.blobs._iot_endpoint_config == null The fundamental reason is that we want to verify that the state block only shows up when expected.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a check in here that the system.last_update time of the state message matches the config timestamp from the new endpoint (so might need to indicate that too). Goal is to check that they don't just connect and then use the wrong reply.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long does phase: final stay in the state message for? Atleast 1? Noting also that the new endpoint probably won't have a blobset.blob._iot_endpoint_config blob

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added last_update and timestamps for config and state messages

```

### Invalid Endpoint (Unsuccessful Reconfiguration)
grafnu marked this conversation as resolved.
Show resolved Hide resolved

```mermaid
%%{wrap}%%
sequenceDiagram
autonumber
participant D as Device
participant E as Original Endpoint
participant E' as New Endpoint
E->>D:CONFIG MESSAGE<br>blobset.blobs._iot_endpoint_config.blob = <ENDPOINT><br>blobset.blobs._iot_endpoint.blob.phase = "final"
D->>E:STATE MESSAGE<BR>blobset.blobs._iot_endpoint_config.phase = "apply"
D-->>E':CONNECTION ATTEMPT
note over D: Failure, e.g. endpoint doesn't exist, incorrect credentials, ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a time limit here? I'd be worried that somebody implements a "try for 3h before reporting failure" scenario... so we should just have a fixed time-bound (which will be needed for the test anyway)... 30s? We can always increase it later, and devices can always report failure faster.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note for 30 seconds

D-->>E:CONNECTION ATTEMPT
E->>D:CONFIG MESSAGE
D->>E:STATE MESSAGE<BR>blobset.blobs._iot_endpoint_config.phase = "final"<BR/>blobset.blobs._iot_endpoint_config.status.level=500 (ERROR)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhere we need to introduce a category(s) for this too...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we differentiate for different types of failure? Or is it a single endpoint.reconfig.failed or similar?
A pretty common error I've encountered before is an endpoint "unreachable" because of firewall or network restrictions. Would that be its own category, or is that level of detail left to the message?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category added


```

## Message Examples

Config message to initiate Reconfiguration (sequence #1 in diagrams above)
<!--example:config/endpoint_reconfiguration.json-->
```json
{
"version": 1,
"blobset": {
"blobs": {
"_iot_endpoint_config": {
"phase": "final",
"content_type": "application/json",
"base64": "ewogICJwcm90b2NvbCI6ICJtcXR0IiwKICAiY2xpZW50X2lkIjogInByb2plY3RzL2Jvcy1zbm9yay1kZXYvbG9jYXRpb25zL3VzLWNlbnRyYWwxL3JlZ2lzdHJpZXMvWlotVFJJLUZFQ1RBL2RldmljZXMvQUhVLTEiLAogICJob3N0bmFtZSI6ICJtcXR0Lmdvb2dsZWFwaXMuY29tIgp9"
}
}
},
"timestamp": "2022-07-13T12:00:00.000Z"
}
```

The base64 encoded value decodes to:
<!--example:configuration_endpoint/simple.json-->
```json
{
"protocol": "mqtt",
"client_id": "projects/bos-snork-dev/locations/us-central1/registries/ZZ-TRI-FECTA/devices/AHU-1",
"hostname": "mqtt.googleapis.com"
}
```

Example successful state message sent to the new endpoint from device following
a successful reconfiguration
<!--example:state/endpoint_reconfiguration.json-->
```json
{
"version": 1,
"timestamp": "2022-07-13T12:00:10.000Z",
"system": {
"hardware": {
"make": "ACME",
"model": "Bird Trap"
},
"software": {
"firmware": "1.2"
},
"serial_no": "000000",
"last_config": "2022-07-13T12:00:00.000Z",
"operational": true
},
"blobset": {
"blobs": {
"_iot_endpoint_config": {
"phase": "final"
}
}
}
}
```

This is an example of the state message sent to the original endpoint after a failure
<!--example:state/endpoint_reconfiguration_failed.json-->
```json
{
"version": 1,
"timestamp": "2022-07-13T12:00:11.000Z",
"system": {
"hardware": {
"make": "ACME",
"model": "Bird Trap"
},
"software": {
"firmware": "1.2"
},
"serial_no": "000000",
"last_config": "2022-07-13T12:00:00.000Z",
"operational": true
},
"blobset": {
"blobs": {
"_iot_endpoint_config": {
"phase": "final",
"status": {
"message": "Could not connect to new endpoint",
"category": "blobset.blob.fail",
"timestamp": "2022-07-13T12:00:11.000Z",
"level": 500
}
}
}
}
}
```
13 changes: 13 additions & 0 deletions tests/config.tests/endpoint_reconfiguration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"blobset": {
"blobs": {
"_iot_endpoint_config": {
noursaidi marked this conversation as resolved.
Show resolved Hide resolved
"phase": "final",
"content_type": "application/json",
"base64": "ewogICJwcm90b2NvbCI6ICJtcXR0IiwKICAiY2xpZW50X2lkIjogInByb2plY3RzL2Jvcy1zbm9yay1kZXYvbG9jYXRpb25zL3VzLWNlbnRyYWwxL3JlZ2lzdHJpZXMvWlotVFJJLUZFQ1RBL2RldmljZXMvQUhVLTEiLAogICJob3N0bmFtZSI6ICJtcXR0Lmdvb2dsZWFwaXMuY29tIgp9"
}
}
},
"timestamp": "2022-07-13T12:00:00.000Z"
}
Empty file.
23 changes: 23 additions & 0 deletions tests/state.tests/endpoint_reconfiguration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": 1,
"timestamp": "2022-07-13T12:00:10.000Z",
"system": {
"hardware": {
"make": "ACME",
"model": "Bird Trap"
},
"software": {
"firmware": "1.2"
},
"serial_no": "000000",
"last_config": "2022-07-13T12:00:00.000Z",
"operational": true
},
"blobset": {
"blobs": {
"_iot_endpoint_config": {
"phase": "final"
noursaidi marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Empty file.
29 changes: 29 additions & 0 deletions tests/state.tests/endpoint_reconfiguration_failed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": 1,
"timestamp": "2022-07-13T12:00:11.000Z",
"system": {
"hardware": {
"make": "ACME",
"model": "Bird Trap"
},
"software": {
"firmware": "1.2"
},
"serial_no": "000000",
"last_config": "2022-07-13T12:00:00.000Z",
"operational": true
},
"blobset": {
"blobs": {
"_iot_endpoint_config": {
"phase": "final",
"status": {
"message": "Could not connect to new endpoint",
"category": "blobset.blob.failure",
"timestamp": "2022-07-13T12:00:11.000Z",
"level": 500
}
}
}
}
}
Empty file.