Skip to content

Commit 95b49eb

Browse files
Merge pull request #568 from linode/dev
Release v5.47.0
2 parents e83d0fe + ccefff3 commit 95b49eb

File tree

17 files changed

+506
-84
lines changed

17 files changed

+506
-84
lines changed

.github/workflows/e2e-suite.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ jobs:
1414
steps:
1515
- name: Clone Repository
1616
uses: actions/checkout@v3
17+
with:
18+
fetch-depth: 0
19+
submodules: 'recursive'
1720

1821
- name: Update system packages
1922
run: sudo apt-get update -y
@@ -50,7 +53,7 @@ jobs:
5053
- name: Add additional information to XML report
5154
run: |
5255
filename=$(ls | grep -E '^[0-9]{12}_cli_test_report\.xml$')
53-
python scripts/add_to_xml_test_report.py \
56+
python tod_scripts/add_to_xml_test_report.py \
5457
--branch_name "${GITHUB_REF#refs/*/}" \
5558
--gha_run_id "$GITHUB_RUN_ID" \
5659
--gha_run_number "$GITHUB_RUN_NUMBER" \
@@ -59,9 +62,8 @@ jobs:
5962
- name: Upload test results
6063
run: |
6164
filename=$(ls | grep -E '^[0-9]{12}_cli_test_report\.xml$')
62-
linode-cli obj --cluster us-southeast-1 put "${filename}" dx-test-results
65+
python tod_scripts/test_report_upload_script.py "${filename}"
6366
env:
64-
LINODE_CLI_TOKEN: ${{ secrets.SHARED_DX_TOKEN }}
6567
LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }}
6668
LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }}
6769

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "test/test_helper/bats-support"]
55
path = test/test_helper/bats-support
66
url = https://github.com/ztombol/bats-support
7+
[submodule "tod_scripts"]
8+
path = tod_scripts
9+
url = https://github.com/linode/TOD-test-report-uploader.git

linodecli/arg_helpers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import os
77
import sys
8+
import textwrap
89
from importlib import import_module
910

1011
import requests
@@ -349,13 +350,30 @@ def action_help(cli, command, action):
349350
except ValueError:
350351
return
351352
print(f"linode-cli {command} {action}", end="")
353+
352354
for param in op.params:
353355
pname = param.name.upper()
354356
print(f" [{pname}]", end="")
357+
355358
print()
356359
print(op.summary)
360+
357361
if op.docs_url:
358-
print(f"API Documentation: {op.docs_url}")
362+
rprint(f"API Documentation: [link={op.docs_url}]{op.docs_url}[/link]")
363+
364+
if len(op.samples) > 0:
365+
print()
366+
print(f"Example Usage{'s' if len(op.samples) > 1 else ''}: ")
367+
368+
rprint(
369+
*[
370+
# Indent all samples for readability; strip and trailing newlines
371+
textwrap.indent(v.get("source").rstrip(), " ")
372+
for v in op.samples
373+
],
374+
sep="\n\n",
375+
)
376+
359377
print()
360378
if op.method == "get" and op.action == "list":
361379
filterable_attrs = [

linodecli/baked/operation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,13 @@ def __init__(self, command, operation: Operation, method, params):
311311
)
312312
self.docs_url = docs_url
313313

314+
code_samples_ext = operation.extensions.get("code-samples")
315+
self.samples = (
316+
[v for v in code_samples_ext if v.get("lang").lower() == "cli"]
317+
if code_samples_ext is not None
318+
else []
319+
)
320+
314321
@property
315322
def args(self):
316323
"""

linodecli/configuration/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,21 +304,20 @@ def configure(
304304

305305
if ENV_TOKEN_NAME in os.environ:
306306
print(
307-
f"""Using token from {ENV_TOKEN_NAME}.
308-
Note that no token will be saved in your configuration file.
309-
* If you lose or remove {ENV_TOKEN_NAME}.
310-
* All profiles will use {ENV_TOKEN_NAME}."""
307+
f"Using token from {ENV_TOKEN_NAME}.\n"
308+
"Note that no token will be saved in your configuration file.\n"
309+
f" * If you lose or remove {ENV_TOKEN_NAME}.\n"
310+
f" * All profiles will use {ENV_TOKEN_NAME}."
311311
)
312312
username = "DEFAULT"
313313
token = os.getenv(ENV_TOKEN_NAME)
314314

315315
else:
316316
if _check_browsers() and not self.configure_with_pat:
317317
print(
318-
"""
319-
The CLI will use its web-based authentication to log you in.
320-
If you prefer to supply a Personal Access Token, use `linode-cli configure --token`.
321-
"""
318+
"The CLI will use its web-based authentication to log you in.\n"
319+
"If you prefer to supply a Personal Access Token,"
320+
"use `linode-cli configure --token`."
322321
)
323322
input(
324323
"Press enter to continue. "

linodecli/plugins/image-upload.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def call(args, context):
9494
nargs="?",
9595
help="A description for this Image. Blank if omitted.",
9696
)
97+
parser.add_argument(
98+
"--cloud-init",
99+
action="store_true",
100+
help="If given, the new image will be flagged as cloud-init compatible.",
101+
)
97102
parser.add_argument(
98103
"file",
99104
metavar="FILE",
@@ -149,6 +154,9 @@ def call(args, context):
149154
if parsed.description:
150155
call_args += ["--description", parsed.description]
151156

157+
if parsed.cloud_init:
158+
call_args += ["--cloud_init", "true"]
159+
152160
status, resp = context.client.call_operation("images", "upload", call_args)
153161

154162
if status != 200:

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}")

linodecli/plugins/obj/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ def call(
531531
"""
532532
This is called when the plugin is invoked
533533
"""
534+
is_help = "--help" in args or "-h" in args
535+
534536
if not HAS_BOTO:
535537
# we can't do anything - ask for an install
536538
print(
@@ -540,7 +542,7 @@ def call(
540542

541543
sys.exit(2) # requirements not met - we can't go on
542544

543-
clusters = get_available_cluster(context.client)
545+
clusters = get_available_cluster(context.client) if not is_help else None
544546
parser = get_obj_args_parser(clusters)
545547
parsed, args = parser.parse_known_args(args)
546548

@@ -556,7 +558,7 @@ def call(
556558
secret_key = None
557559

558560
# make a client, but only if we weren't printing help
559-
if not "--help" in args:
561+
if not is_help:
560562
access_key, secret_key = get_credentials(context.client)
561563

562564
cluster = parsed.cluster

requirements-dev.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@ requests-mock==1.11.0
88
boto3-stubs[s3]
99
build>=0.10.0
1010
twine>=4.0.2
11-
packaging>=23.2

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

0 commit comments

Comments
 (0)