Update docker-py version, use MVC pattern, automatically select file type

This commit is contained in:
LeoVerto 2018-11-27 07:22:25 +01:00
parent b84d30ee18
commit bd079656a6
4 changed files with 159 additions and 90 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
*.gv
*.pdf
*.png
*.svg
.idea

View file

@ -4,10 +4,10 @@ verify_ssl = true
name = "pypi"
[packages]
docker-py = "*"
docker = "*"
graphviz = "*"
[dev-packages]
[requires]
python_version = "3"
python_version = "3.7"

43
Pipfile.lock generated
View file

@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "b980cd984b5794dc93711b11dcaadee33a6ee25a811c7aa8c8a6c7db32a78204"
"sha256": "9ea88e9a4fb06636a8077b1f1bc6e322fa52659b9159fffa1260d5ad8225ddd4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
"python_version": "3.7"
},
"sources": [
{
@ -18,10 +18,10 @@
"default": {
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.8.24"
"version": "==2018.10.15"
},
"chardet": {
"hashes": [
@ -30,13 +30,13 @@
],
"version": "==3.0.4"
},
"docker-py": {
"docker": {
"hashes": [
"sha256:35b506e95861914fa5ad57a6707e3217b4082843b883be246190f57013948aba",
"sha256:4c2a75875764d38d67f87bc7d03f7443a3895704efc57962bdf6500b8d4bc415"
"sha256:31421f16c01ffbd1ea7353c7e7cd7540bf2e5906d6173eb51c8fea4e0ea38b19",
"sha256:fbe82af9b94ccced752527c8de07fa20267f9634b48674ba478a0bb4000a0b1e"
],
"index": "pypi",
"version": "==1.10.6"
"version": "==3.5.1"
},
"docker-pycreds": {
"hashes": [
@ -47,11 +47,11 @@
},
"graphviz": {
"hashes": [
"sha256:310bacfb969f0ac7c872610500e017c3e82b24a8abd33d289e99af162de30cb8",
"sha256:865afa6ab9775cf29db03abd8e571a164042c726c35a1b3c1e2b8c4c645e2993"
"sha256:0e1744a45b0d707bc44f99c7b8e5f25dc22cf96b6aaf2432ac308ed9822a9cb6",
"sha256:d311be4fddfe832a56986ac5e1d6e8715d7fcb0208560da79d1bb0f72abef41f"
],
"index": "pypi",
"version": "==0.9"
"version": "==0.10.1"
},
"idna": {
"hashes": [
@ -62,10 +62,10 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54",
"sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263"
],
"version": "==2.19.1"
"version": "==2.20.1"
},
"six": {
"hashes": [
@ -76,18 +76,17 @@
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"markers": "python_version < '4' and python_version != '3.1.*' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*'",
"version": "==1.23"
"version": "==1.24.1"
},
"websocket-client": {
"hashes": [
"sha256:030bbfbf29ac9e315ffb207ed5ed42b6981b5038ea00d1e13b02b872cc95e8f6",
"sha256:a35bac3d9647c62c1ba3e8a7340385d92981f5486b033557d592138fd4b21b90"
"sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786",
"sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849"
],
"version": "==0.51.0"
"version": "==0.54.0"
}
},
"develop": {}

View file

@ -1,10 +1,13 @@
#!/usr/bin/python3
import os
import json
import argparse
import random
from docker import Client
import docker
import typing
from dataclasses import dataclass
from graphviz import Graph
from graphviz.backend import FORMATS
# colorlover.scales["12"]["qual"]["Paired"] converted to hex strings
COLORS = ["#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928", "#a6cee3", "#b2df8a", "#fdbf6f",
@ -12,7 +15,36 @@ COLORS = ["#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928", "#a6
i = 0
def get_unique_color():
@dataclass
class Network:
name: str
gateway: str
internal: bool
isolated: bool
color: str
@dataclass
class Interface:
endpoint_id: str
address: str
@dataclass
class Container:
container_id: str
name: str
interfaces: typing.List[Interface]
@dataclass
class Link:
container_id: str
endpoint_id: str
network_name: str
def get_unique_color() -> str:
global i
if i < len(COLORS):
@ -25,98 +57,135 @@ def get_unique_color():
return c
def generate_graph(verbose: bool, file: str):
g = Graph(comment="Docker Network Graph", engine="sfdp", format="png",
graph_attr=dict(splines="true"))
docker_client = Client(os.environ.get("DOCKER_HOST", "unix:///var/run/docker.sock"))
def dump_json(obj):
print(json.dumps(obj, indent=4))
for c in docker_client.containers():
name = c["Names"][0][1:]
container_id = c["Id"]
node_id = f"container_{container_id}"
iface_labels = []
for net_name, net_info in c["NetworkSettings"]["Networks"].items():
label_iface = f"<{net_info['EndpointID']}> {net_info['IPAddress']}"
iface_labels.append(label_iface)
labels = "|".join(iface_labels)
if verbose:
print(labels)
g.node(node_id,
shape="record",
label=f"{{ {name} | { {labels} } }}",
fillcolor="#ff9999",
style="filled"
)
for net in sorted(docker_client.networks(), key=lambda k: k["Name"]):
net_name = net["Name"]
color = get_unique_color()
def get_networks(client: docker.DockerClient, verbose: bool) -> typing.Dict[str, Network]:
networks: typing.Dict[str, Network] = {}
for net in sorted(client.networks.list(), key=lambda k: k.name):
try:
gateway = net["IPAM"]["Config"][0]["Gateway"]
gateway = net.attrs["IPAM"]["Config"][0]["Gateway"]
except (KeyError, IndexError):
# This network doesn't seem to be used, skip it
continue
internal = ""
internal = False
try:
if net["Internal"]:
internal = "| Internal"
if net.attrs["Internal"]:
internal = True
except KeyError:
pass
isolated = ""
isolated = False
try:
if net["Options"]["com.docker.network.bridge.enable_icc"] == "false":
isolated = "| Containers isolated"
if net.attrs["Options"]["com.docker.network.bridge.enable_icc"] == "false":
isolated = True
except KeyError:
pass
if verbose:
print(f"Network: {net_name} {internal} gw:{gateway}")
print(f"Network: {net.name} {'internal' if internal else ''} {'isolated' if isolated else ''} gw:{gateway}")
net_node_id = f"net_{net_name}"
color = get_unique_color()
networks[net.name] = Network(net.name, gateway, internal, isolated, color)
label = f"{{<gw_iface> {gateway} | {net_name} {internal} {isolated}}}"
return networks
g.node(net_node_id,
def get_containers(client: docker.DockerClient, verbose: bool) -> (typing.List[Container], typing.List[Link]):
containers: typing.List[Container] = []
links: typing.List[Link] = []
for container in client.containers.list():
interfaces: typing.List[Interface] = []
# Iterate over container interfaces
for net_name, net_info in container.attrs["NetworkSettings"]["Networks"].items():
endpoint_id = net_info["EndpointID"]
interfaces.append(Interface(endpoint_id, net_info['IPAddress']))
links.append(Link(container.id, endpoint_id, net_name))
if verbose:
print(f"Container: {container.name} {''.join([iface.address for iface in interfaces])}")
containers.append(Container(container.id, container.name, interfaces))
return containers, links
def draw_network(g: Graph, net: Network):
label = f"{{<gw_iface> {net.gateway} | {net.name}"
if net.internal:
label += " | Internal"
if net.isolated:
label += " | Containers isolated"
label += "}"
g.node(f"network_{net.name}",
shape="record",
label=label,
fillcolor=color,
fillcolor=net.color,
style="filled"
)
if net["Containers"]:
for container_id, container in sorted(net["Containers"].items()):
if verbose:
dump_json(container)
print(" * ", container["Name"], container["IPv4Address"], container["IPv6Address"])
container_node_id = f"container_{container_id}"
def draw_container(g: Graph, c: Container):
iface_labels = [f"<{iface.endpoint_id}> {iface.address}" for iface in c.interfaces]
container_iface_ref = f"{container_node_id}:{container['EndpointID']}"
label = f"{{ {c.name} | {{ {'|'.join(iface_labels)} }} }}"
g.edge(container_iface_ref, f"{net_node_id}:gw_iface", color=color)
g.node(f"container_{c.container_id}",
shape="record",
label=label,
fillcolor="#ff9999",
style="filled"
)
def draw_link(g: Graph, networks: typing.Dict[str, Network], link: Link):
g.edge(f"container_{link.container_id}:{link.endpoint_id}",
f"network_{link.network_name}",
color=networks[link.network_name].color
)
def generate_graph(verbose: bool, file: str):
docker_client = docker.from_env()
networks = get_networks(docker_client, verbose)
containers, links = get_containers(docker_client, verbose)
print(g.source)
if file:
g.render(file)
base, ext = os.path.splitext(file)
g = Graph(comment="Docker Network Graph", engine="sfdp", format=ext[1:], graph_attr=dict(splines="true"))
else:
g = Graph(comment="Docker Network Graph", engine="sfdp", graph_attr=dict(splines="true"))
for _, network in networks.items():
draw_network(g, network)
for container in containers:
draw_container(g, container)
for link in links:
draw_link(g, networks, link)
if file:
g.render(base)
else:
print(g.source)
def graphviz_output_file(filename: str):
ext = os.path.splitext(filename)[1][1:]
if ext.lower() not in FORMATS:
raise argparse.ArgumentTypeError("Must be valid graphviz output format")
return filename
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate docker network graph.")
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
parser.add_argument("-o", "--out", help="Write output to file", type=str)
parser.add_argument("-o", "--out", help="Write output to file", type=graphviz_output_file)
args = parser.parse_args()
generate_graph(args.verbose, args.out)