Skip to content

Commit 6193e0f

Browse files
Creating Metadata Plugin (#558)
1 parent 9723d47 commit 6193e0f

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

linodecli/plugins/metadata.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
This plugin allows users to access the metadata service while in a Linode.
3+
4+
Usage:
5+
6+
linode-cli metadata [ENDPOINT]
7+
"""
8+
9+
import argparse
10+
import sys
11+
12+
from linode_metadata import MetadataClient
13+
from linode_metadata.objects.error import ApiError
14+
from linode_metadata.objects.instance import ResponseBase
15+
from requests import ConnectTimeout
16+
from rich import print as rprint
17+
from rich.table import Table
18+
19+
PLUGIN_BASE = "linode-cli metadata"
20+
21+
22+
def process_sub_columns(subcolumn: ResponseBase, table: Table, values_row):
23+
"""
24+
Helper method to process embedded ResponseBase objects
25+
"""
26+
for key, value in vars(subcolumn).items():
27+
if isinstance(value, ResponseBase):
28+
process_sub_columns(value, table, values_row)
29+
else:
30+
table.add_column(key)
31+
values_row.append(str(value))
32+
33+
34+
def print_instance_table(data):
35+
"""
36+
Prints the table that contains information about the current instance
37+
"""
38+
attributes = vars(data)
39+
values_row = []
40+
41+
table = Table()
42+
43+
for key, value in attributes.items():
44+
if isinstance(value, ResponseBase):
45+
process_sub_columns(value, table, values_row)
46+
else:
47+
table.add_column(key)
48+
values_row.append(str(value))
49+
50+
table.add_row(*values_row)
51+
rprint(table)
52+
53+
54+
def print_ssh_keys_table(data):
55+
"""
56+
Prints the table that contains information about the SSH keys for the current instance
57+
"""
58+
table = Table(show_lines=True)
59+
60+
table.add_column("ssh keys")
61+
62+
if data.users.root is not None:
63+
for key in data.users.root:
64+
table.add_row(key)
65+
66+
rprint(table)
67+
68+
69+
def print_networking_tables(data):
70+
"""
71+
Prints the table that contains information about the network of the current instance
72+
"""
73+
interfaces = Table(title="Interfaces", show_lines=True)
74+
75+
interfaces.add_column("label")
76+
interfaces.add_column("purpose")
77+
interfaces.add_column("ipam addresses")
78+
79+
for interface in data.interfaces:
80+
attributes = vars(interface)
81+
interface_row = []
82+
for _, value in attributes.items():
83+
interface_row.append(str(value))
84+
interfaces.add_row(*interface_row)
85+
86+
ipv4 = Table(title="IPv4")
87+
ipv4.add_column("ip address")
88+
ipv4.add_column("type")
89+
attributes = vars(data.ipv4)
90+
for key, value in attributes.items():
91+
for address in value:
92+
ipv4.add_row(*[address, key])
93+
94+
ipv6 = Table(title="IPv6")
95+
ipv6_data = data.ipv6
96+
ipv6.add_column("slaac")
97+
ipv6.add_column("link local")
98+
ipv6.add_column("ranges")
99+
ipv6.add_column("shared ranges")
100+
ipv6.add_row(
101+
*[
102+
ipv6_data.slaac,
103+
ipv6_data.link_local,
104+
str(ipv6_data.ranges),
105+
str(ipv6_data.shared_ranges),
106+
]
107+
)
108+
109+
rprint(interfaces)
110+
rprint(ipv4)
111+
rprint(ipv6)
112+
113+
114+
def get_instance(client: MetadataClient):
115+
"""
116+
Get information about your instance, including plan resources
117+
"""
118+
data = client.get_instance()
119+
print_instance_table(data)
120+
121+
122+
def get_user_data(client: MetadataClient):
123+
"""
124+
Get your user data
125+
"""
126+
data = client.get_user_data()
127+
rprint(data)
128+
129+
130+
def get_network(client: MetadataClient):
131+
"""
132+
Get information about your instance’s IP addresses
133+
"""
134+
data = client.get_network()
135+
print_networking_tables(data)
136+
137+
138+
def get_ssh_keys(client: MetadataClient):
139+
"""
140+
Get information about public SSH Keys configured on your instance
141+
"""
142+
data = client.get_ssh_keys()
143+
print_ssh_keys_table(data)
144+
145+
146+
COMMAND_MAP = {
147+
"instance": get_instance,
148+
"user-data": get_user_data,
149+
"networking": get_network,
150+
"sshkeys": get_ssh_keys,
151+
}
152+
153+
154+
def print_help(parser: argparse.ArgumentParser):
155+
"""
156+
Print out the help info to the standard output
157+
"""
158+
parser.print_help()
159+
160+
# additional help
161+
print()
162+
print("Available endpoints: ")
163+
164+
command_help_map = [
165+
[name, func.__doc__.strip()]
166+
for name, func in sorted(COMMAND_MAP.items())
167+
]
168+
169+
tab = Table(show_header=False)
170+
for row in command_help_map:
171+
tab.add_row(*row)
172+
rprint(tab)
173+
174+
175+
def get_metadata_parser():
176+
"""
177+
Builds argparser for Metadata plug-in
178+
"""
179+
parser = argparse.ArgumentParser(PLUGIN_BASE, add_help=False)
180+
181+
parser.add_argument(
182+
"endpoint",
183+
metavar="ENDPOINT",
184+
nargs="?",
185+
type=str,
186+
help="The API endpoint to be called from the Metadata service.",
187+
)
188+
189+
return parser
190+
191+
192+
def call(args, _):
193+
"""
194+
The entrypoint for this plugin
195+
"""
196+
197+
parser = get_metadata_parser()
198+
parsed, args = parser.parse_known_args(args)
199+
200+
if not parsed.endpoint in COMMAND_MAP or len(args) != 0:
201+
print_help(parser)
202+
sys.exit(0)
203+
204+
# make a client, but only if we weren't printing help and endpoint is valid
205+
if "--help" not in args:
206+
try:
207+
client = MetadataClient()
208+
except ConnectTimeout as exc:
209+
raise ConnectionError(
210+
"Can't access Metadata service. Please verify that you are inside a Linode."
211+
) from exc
212+
else:
213+
print_help(parser)
214+
sys.exit(0)
215+
216+
try:
217+
COMMAND_MAP[parsed.endpoint](client)
218+
except ApiError as e:
219+
sys.exit(f"Error: {e}")

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ PyYAML
44
packaging
55
rich
66
urllib3<3
7+
linode-metadata

tests/unit/test_plugin_metadata.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import importlib
2+
3+
import pytest
4+
from linode_metadata.objects.instance import InstanceResponse
5+
from linode_metadata.objects.networking import NetworkResponse
6+
from linode_metadata.objects.ssh_keys import SSHKeysResponse
7+
from pytest import CaptureFixture
8+
9+
from linodecli.plugins.metadata import (
10+
print_instance_table,
11+
print_networking_tables,
12+
print_ssh_keys_table,
13+
)
14+
15+
plugin = importlib.import_module("linodecli.plugins.metadata")
16+
17+
INSTANCE = InstanceResponse(
18+
json_data={
19+
"id": 1,
20+
"host_uuid": "test_uuid",
21+
"label": "test-label",
22+
"region": "us-southeast",
23+
"tags": "test-tag",
24+
"type": "g6-standard-1",
25+
"specs": {"vcpus": 2, "disk": 3, "memory": 4, "transfer": 5, "gpus": 6},
26+
"backups": {"enabled": False, "status": ["test1", "test2"]},
27+
}
28+
)
29+
30+
NETWORKING = NetworkResponse(
31+
json_data={
32+
"interfaces": [
33+
{
34+
"label": "interface-label-1",
35+
"purpose": "purpose-1",
36+
"ipam_address": ["address1", "address2"],
37+
},
38+
{
39+
"label": "interface-label-2",
40+
"purpose": "purpose-2",
41+
"ipam_address": ["address3", "address4"],
42+
},
43+
],
44+
"ipv4": {
45+
"public": ["public-1", "public-2"],
46+
"private": ["private-1", "private-2"],
47+
"shared": ["shared-1", "shared-2"],
48+
},
49+
"ipv6": {
50+
"slaac": "slaac-1",
51+
"link_local": "link-local-1",
52+
"ranges": ["range-1", "range-2"],
53+
"shared_ranges": ["shared-range-1", "shared-range-2"],
54+
},
55+
}
56+
)
57+
58+
SSH_KEYS = SSHKeysResponse(
59+
json_data={"users": {"root": ["ssh-key-1", "ssh-key-2"]}}
60+
)
61+
62+
SSH_KEYS_EMPTY = SSHKeysResponse(json_data={"users": {"root": None}})
63+
64+
65+
def test_print_help(capsys: CaptureFixture):
66+
with pytest.raises(SystemExit) as err:
67+
plugin.call(["--help"], None)
68+
69+
captured_text = capsys.readouterr().out
70+
71+
assert err.value.code == 0
72+
assert "Available endpoints: " in captured_text
73+
assert "Get information about public SSH Keys" in captured_text
74+
75+
76+
def test_faulty_endpoint(capsys: CaptureFixture):
77+
with pytest.raises(SystemExit) as err:
78+
plugin.call(["blah"], None)
79+
80+
captured_text = capsys.readouterr().out
81+
82+
assert err.value.code == 0
83+
assert "Available endpoints: " in captured_text
84+
assert "Get information about public SSH Keys" in captured_text
85+
86+
87+
def test_instance_table(capsys: CaptureFixture):
88+
# Note: Test is brief since table is very large with all values included and captured text abbreviates a lot of values
89+
print_instance_table(INSTANCE)
90+
captured_text = capsys.readouterr()
91+
92+
assert "id" in captured_text.out
93+
assert "1" in captured_text.out
94+
95+
assert "3" in captured_text.out
96+
assert "2" in captured_text.out
97+
98+
99+
def test_networking_table(capsys: CaptureFixture):
100+
print_networking_tables(NETWORKING)
101+
captured_text = capsys.readouterr()
102+
103+
assert "purpose" in captured_text.out
104+
assert "purpose-1" in captured_text.out
105+
106+
assert "ip address" in captured_text.out
107+
assert "private-1" in captured_text.out
108+
assert "type" in captured_text.out
109+
assert "shared" in captured_text.out
110+
111+
assert "slaac" in captured_text.out
112+
assert "slaac-1" in captured_text.out
113+
114+
115+
def test_ssh_key_table(capsys: CaptureFixture):
116+
print_ssh_keys_table(SSH_KEYS)
117+
captured_text = capsys.readouterr()
118+
119+
assert "ssh keys" in captured_text.out
120+
assert "ssh-key-1" in captured_text.out
121+
assert "ssh-key-2" in captured_text.out
122+
123+
124+
def test_empty_ssh_key_table(capsys: CaptureFixture):
125+
print_ssh_keys_table(SSH_KEYS_EMPTY)
126+
captured_text = capsys.readouterr()
127+
128+
assert "ssh keys" in captured_text.out

0 commit comments

Comments
 (0)