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 *.gv
*.pdf *.pdf
*.png *.png
*.svg
.idea .idea

View file

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

43
Pipfile.lock generated
View file

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

View file

@ -1,10 +1,13 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import json
import argparse import argparse
import random import random
from docker import Client import docker
import typing
from dataclasses import dataclass
from graphviz import Graph from graphviz import Graph
from graphviz.backend import FORMATS
# colorlover.scales["12"]["qual"]["Paired"] converted to hex strings # colorlover.scales["12"]["qual"]["Paired"] converted to hex strings
COLORS = ["#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928", "#a6cee3", "#b2df8a", "#fdbf6f", COLORS = ["#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928", "#a6cee3", "#b2df8a", "#fdbf6f",
@ -12,7 +15,36 @@ COLORS = ["#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928", "#a6
i = 0 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 global i
if i < len(COLORS): if i < len(COLORS):
@ -25,98 +57,135 @@ def get_unique_color():
return c return c
def generate_graph(verbose: bool, file: str): def get_networks(client: docker.DockerClient, verbose: bool) -> typing.Dict[str, Network]:
g = Graph(comment="Docker Network Graph", engine="sfdp", format="png", networks: typing.Dict[str, Network] = {}
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()
for net in sorted(client.networks.list(), key=lambda k: k.name):
try: try:
gateway = net["IPAM"]["Config"][0]["Gateway"] gateway = net.attrs["IPAM"]["Config"][0]["Gateway"]
except (KeyError, IndexError): except (KeyError, IndexError):
# This network doesn't seem to be used, skip it # This network doesn't seem to be used, skip it
continue continue
internal = "" internal = False
try: try:
if net["Internal"]: if net.attrs["Internal"]:
internal = "| Internal" internal = True
except KeyError: except KeyError:
pass pass
isolated = "" isolated = False
try: try:
if net["Options"]["com.docker.network.bridge.enable_icc"] == "false": if net.attrs["Options"]["com.docker.network.bridge.enable_icc"] == "false":
isolated = "| Containers isolated" isolated = True
except KeyError: except KeyError:
pass pass
if verbose: 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,
shape="record",
label=label,
fillcolor=color,
style="filled"
)
if net["Containers"]: def get_containers(client: docker.DockerClient, verbose: bool) -> (typing.List[Container], typing.List[Link]):
for container_id, container in sorted(net["Containers"].items()): containers: typing.List[Container] = []
if verbose: links: typing.List[Link] = []
dump_json(container)
print(" * ", container["Name"], container["IPv4Address"], container["IPv6Address"])
container_node_id = f"container_{container_id}" for container in client.containers.list():
interfaces: typing.List[Interface] = []
container_iface_ref = f"{container_node_id}:{container['EndpointID']}" # Iterate over container interfaces
for net_name, net_info in container.attrs["NetworkSettings"]["Networks"].items():
endpoint_id = net_info["EndpointID"]
g.edge(container_iface_ref, f"{net_node_id}:gw_iface", color=color) 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=net.color,
style="filled"
)
def draw_container(g: Graph, c: Container):
iface_labels = [f"<{iface.endpoint_id}> {iface.address}" for iface in c.interfaces]
label = f"{{ {c.name} | {{ {'|'.join(iface_labels)} }} }}"
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: 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__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate docker network graph.") parser = argparse.ArgumentParser(description="Generate docker network graph.")
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true") 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() args = parser.parse_args()
generate_graph(args.verbose, args.out) generate_graph(args.verbose, args.out)