Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Commit

Permalink
Bonsai v2 brains and iotedge support (#69)
Browse files Browse the repository at this point in the history
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
6 people authored Jan 5, 2022
1 parent 735f22a commit 1625f0a
Show file tree
Hide file tree
Showing 10 changed files with 646 additions and 254 deletions.
239 changes: 239 additions & 0 deletions bin/brains
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()
2 changes: 1 addition & 1 deletion bin/splash
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fw_version="$([ -e ~/moab/bin/fwversion ] && ~/moab/bin/fwversion | awk '{print

cat <<EOF
${h2} · IP ${r}${ip}
${h2} · IP ${r}http://${ip}
${h2} · Kernel ${r}${kernel}
${h2} · Load ${r}${load}
${h2} · CPU ${r}${temps}
Expand Down
68 changes: 0 additions & 68 deletions os/azureiot

This file was deleted.

35 changes: 0 additions & 35 deletions os/brains

This file was deleted.

8 changes: 8 additions & 0 deletions os/files/home/bashrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ function cd {
builtin cd "$@" && ls $lsflags
}

function dps () {
command docker ps --all --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}" \
| (echo -e "CONTAINER_ID\tNAMES\tIMAGE\tPORTS\tSTATUS" && cat) \
| awk '{printf "\033[1;32m%s\t\033[01;38;5;95;38;5;196m%s\t\033[00m\033[1;34m%s\t\033[01;90m%s %s %s %s %s %s %s\033[00m\n", $1, $2, $3, $4, $5, $6, $7, $8, $9, $10;}' \
| column -s$'\t' -t \
| awk 'NR<2{print $0;next}{print $0 | "sort --key=2"}'
}

if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
fi
Expand Down
Loading

0 comments on commit 1625f0a

Please sign in to comment.