This repository has been archived by the owner on Feb 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bonsai v2 brains and iotedge support (#69)
Major: * v2 support for Bonsai API brains * new `brains` CLI shows moby-engine and iotedge managed brains Note: Azure IOT Edge is supported, but not installed by default. Co-authored-by: JRAlexander <[email protected]> Co-authored-by: Scott Stanfield <[email protected]> Co-authored-by: Scott Stanfield <[email protected]> Co-authored-by: Kirill Polzounov <[email protected]> Co-authored-by: Journey McDowell <[email protected]>
- Loading branch information
1 parent
735f22a
commit 1625f0a
Showing
10 changed files
with
646 additions
and
254 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
#!/usr/bin/python3 | ||
|
||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
import json | ||
import argparse | ||
from os import pipe | ||
import requests | ||
import subprocess | ||
from dataclasses import dataclass | ||
|
||
@dataclass | ||
class BonsaiImage: | ||
image: str # image scotstanws.azurecr.io/d83f2142-1c3c-4ae4-84fa-4e9ff3aa5ed9/circle:2-linux-arm32v7 | ||
brain_id: str # 'circle' | ||
version: str # 2 or empty '' | ||
short_name: str # 'circle:2' (maximum of 9 chars if no version else truncated to 6 chars) | ||
port: int # port number | ||
iotedge: bool # true if this brain is managed by IOT Edge | ||
|
||
|
||
def ps(): | ||
docker_ps = subprocess.Popen( | ||
"docker ps --format '{{json .}}'", | ||
stdout=subprocess.PIPE, | ||
shell=True, | ||
universal_newlines=True, | ||
) | ||
|
||
stdout = docker_ps.communicate()[0] | ||
|
||
if docker_ps.returncode == 0: | ||
docker_images = json.loads(reformat_json(stdout)) | ||
bonsai_images = list_to_bonsai_images(docker_images) | ||
return bonsai_images | ||
|
||
|
||
def reformat_json(stdout): | ||
# docker ps returns invalid json | ||
# split the json objects on the newline | ||
|
||
json = "[" | ||
lines = stdout.splitlines() | ||
for i, line in enumerate(lines): | ||
json += line | ||
# add comma between each json object | ||
if i != len(lines) - 1: | ||
json += "," | ||
|
||
# Add ending bracket for well-formed json | ||
json += "]" | ||
return json | ||
|
||
|
||
def get_port(splitports): | ||
# port format: 'Ports': '0.0.0.0:5005->5000/tcp, :::5005->5000/tcp' | ||
if splitports is not None: | ||
# split on comma first - 0.0.0.0:5005->5000/tcp | ||
splitport_1 = splitports.split(",")[0] | ||
# split next on arrow - 0.0.0.0:5005 | ||
splitport_2 = splitport_1.split("->")[0] | ||
# finally split on colon and take last element - 5005 | ||
port = splitport_2.split(":")[1] | ||
else: | ||
port = 0 | ||
|
||
return port | ||
|
||
|
||
def get_resp(port, client_id=12345): | ||
# Test that port has a valid brain | ||
resp_status = requests.get(f"http://localhost:{port}/v1/status").status_code | ||
valid_brain = resp_status == 200 | ||
|
||
if not valid_brain: | ||
raise ValueError(f"Port {port} is not a valid Bonsai brain") | ||
|
||
# Test whether the brain is v1 or v2 (also resets memory if a v2 brain) | ||
resp_delete = requests.delete(f"http://localhost:{port}/v2/clients/{client_id}").status_code | ||
version = 2 if resp_delete == 204 else 1 | ||
|
||
if version == 1: | ||
prediction_url = f"http://localhost:{port}/v1/prediction" | ||
elif version == 2: | ||
prediction_url = f"http://localhost:{port}/v2/clients/{client_id}/predict" | ||
else: | ||
raise ValueError("Brain version `{version}` is not supported.") | ||
|
||
return version, resp_status, resp_delete | ||
|
||
|
||
def get_api_url(port, version): | ||
process = subprocess.Popen( | ||
"ip route get 1.1.1.1 | head -1 | cut -d' ' -f7", | ||
stdout=subprocess.PIPE, | ||
shell=True, | ||
universal_newlines=True, | ||
) | ||
ip, stderr = process.communicate() | ||
ip = ip.strip() # remove newline | ||
|
||
if version == 1: | ||
api_url = f"http://{ip}:{port}/v1/doc/index.html" | ||
elif version == 2: | ||
api_url = f"http://{ip}:{port}/swagger.html" | ||
else: | ||
raise ValueError("Brain version `{version}` is not supported.") | ||
return api_url | ||
|
||
|
||
def get_image_info(info, port): | ||
image_name = info["Image"] | ||
container_name = info["Names"] | ||
|
||
# Use image name if there are no container names | ||
if container_name is None: | ||
container_name = image_name | ||
|
||
# split image on slashes | ||
version = 0 | ||
slashes = image_name.split("/") | ||
|
||
# if image tag or no slashes, use the image name | ||
if slashes is not None: | ||
if len(slashes) == 1: | ||
brain_id = image_name | ||
short_name = container_name[:9] | ||
|
||
else: | ||
# if there's a colon in the name, use it for version | ||
colon = slashes[-1].split(":") | ||
|
||
if colon and len(colon) > 1: | ||
version_split = colon[1].split("-") | ||
# if version_split, account for dashes | ||
if version_split is not None: | ||
version = version_split[0] | ||
brain_id = colon[0] | ||
short_name = container_name[:6] + ":" + version | ||
else: | ||
brain_id = colon[0] | ||
short_name = container_name[:9] | ||
else: | ||
# brain_id will be the last split on slashes | ||
brain_id = slashes[-1] | ||
short_name = container_name[:9] | ||
|
||
bonsai_image = BonsaiImage( | ||
image=image_name, | ||
brain_id=brain_id, | ||
version=version, | ||
short_name=short_name, | ||
port=int(port), | ||
iotedge=info["Networks"] == "azure-iot-edge", | ||
) | ||
return bonsai_image | ||
|
||
|
||
def list_to_bonsai_images(iot_dict): | ||
# list of BonsaiImages to return | ||
bonsai_images = [] | ||
|
||
# parse the iot_dict list | ||
for info in iot_dict: | ||
if (info["Names"] != "edgeHub") and (info["Names"] != "edgeAgent"): | ||
# check for port | ||
if "Ports" in info.keys(): | ||
port = get_port(info["Ports"]) | ||
bonsai_image = get_image_info(info, port) | ||
bonsai_images.append(bonsai_image) | ||
|
||
return bonsai_images | ||
|
||
|
||
def brains(): | ||
title = ["PORT ", "NAME ", "VER", "IOT EDGE", "API URL"] | ||
widths = [len(el) + 3 for el in title] | ||
|
||
# Print the column titles with a minimum of 3 spaces in between each column | ||
print("".join(x.ljust(width) for x, width in zip(title, widths))) | ||
# Print a line of dashes matching the column titles | ||
print("".join((len(x) * "-").ljust(width) for x, width in zip(title, widths))) | ||
|
||
images = sorted(ps(), key=lambda x: x.port) | ||
for x in images: | ||
brain_name = x.short_name | ||
bonsai_version, resp_status, resp_delete = get_resp(x.port) | ||
brain_url = x.image | ||
|
||
if len(brain_name) > 9: | ||
brain_name = brain_name[:len(title[1]) - 3] + "..." | ||
|
||
print( | ||
str(x.port).ljust(widths[0]) | ||
+ brain_name.ljust(widths[1]) | ||
+ f"v{bonsai_version}".ljust(widths[2]) | ||
+ ("*" if x.iotedge else "").ljust(widths[3]) | ||
+ get_api_url(x.port, bonsai_version).ljust(widths[4]) | ||
) | ||
|
||
|
||
def brains_long(): | ||
title = ["PORT ", "NAME ", "VER", "IOT EDGE", "RESP STATUS", "RESP DELETE", "BRAIN IMAGE"] | ||
widths = [len(el) + 3 for el in title] | ||
|
||
# Print the column titles with a minimum of 3 spaces in between each column | ||
print("".join(x.ljust(width) for x, width in zip(title, widths))) | ||
# Print a line of dashes matching the column titles | ||
print("".join((len(x) * "-").ljust(width) for x, width in zip(title, widths))) | ||
|
||
images = sorted(ps(), key=lambda x: x.port) | ||
for x in images: | ||
brain_name = x.short_name | ||
bonsai_version, resp_status, resp_delete = get_resp(x.port) | ||
brain_url = x.image | ||
|
||
if len(brain_name) > widths[1]: | ||
brain_name = brain_name[:len(title[1]) - 3] + "..." | ||
|
||
print( | ||
str(x.port).ljust(widths[0]) | ||
+ brain_name.ljust(widths[1]) | ||
+ f"v{bonsai_version}".ljust(widths[2]) | ||
+ ("*" if x.iotedge else "").ljust(widths[3]) | ||
+ str(resp_status).ljust(widths[4]) | ||
+ str(resp_delete).ljust(widths[5]) | ||
+ brain_url.ljust(widths[6]) | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument('-l', action='store_true') | ||
args = parser.parse_args() | ||
if args.l: | ||
brains_long() | ||
else: | ||
brains() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.