Skip to content

Commit

Permalink
Merge pull request #568 from linode/dev
Browse files Browse the repository at this point in the history
Release v5.47.0
  • Loading branch information
zliang-akamai authored Jan 9, 2024
2 parents e83d0fe + ccefff3 commit 95b49eb
Show file tree
Hide file tree
Showing 17 changed files with 506 additions and 84 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: 'recursive'

- name: Update system packages
run: sudo apt-get update -y
Expand Down Expand Up @@ -50,7 +53,7 @@ jobs:
- name: Add additional information to XML report
run: |
filename=$(ls | grep -E '^[0-9]{12}_cli_test_report\.xml$')
python scripts/add_to_xml_test_report.py \
python tod_scripts/add_to_xml_test_report.py \
--branch_name "${GITHUB_REF#refs/*/}" \
--gha_run_id "$GITHUB_RUN_ID" \
--gha_run_number "$GITHUB_RUN_NUMBER" \
Expand All @@ -59,9 +62,8 @@ jobs:
- name: Upload test results
run: |
filename=$(ls | grep -E '^[0-9]{12}_cli_test_report\.xml$')
linode-cli obj --cluster us-southeast-1 put "${filename}" dx-test-results
python tod_scripts/test_report_upload_script.py "${filename}"
env:
LINODE_CLI_TOKEN: ${{ secrets.SHARED_DX_TOKEN }}
LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }}
LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }}

Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "test/test_helper/bats-support"]
path = test/test_helper/bats-support
url = https://github.com/ztombol/bats-support
[submodule "tod_scripts"]
path = tod_scripts
url = https://github.com/linode/TOD-test-report-uploader.git
20 changes: 19 additions & 1 deletion linodecli/arg_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import os
import sys
import textwrap
from importlib import import_module

import requests
Expand Down Expand Up @@ -349,13 +350,30 @@ def action_help(cli, command, action):
except ValueError:
return
print(f"linode-cli {command} {action}", end="")

for param in op.params:
pname = param.name.upper()
print(f" [{pname}]", end="")

print()
print(op.summary)

if op.docs_url:
print(f"API Documentation: {op.docs_url}")
rprint(f"API Documentation: [link={op.docs_url}]{op.docs_url}[/link]")

if len(op.samples) > 0:
print()
print(f"Example Usage{'s' if len(op.samples) > 1 else ''}: ")

rprint(
*[
# Indent all samples for readability; strip and trailing newlines
textwrap.indent(v.get("source").rstrip(), " ")
for v in op.samples
],
sep="\n\n",
)

print()
if op.method == "get" and op.action == "list":
filterable_attrs = [
Expand Down
7 changes: 7 additions & 0 deletions linodecli/baked/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ def __init__(self, command, operation: Operation, method, params):
)
self.docs_url = docs_url

code_samples_ext = operation.extensions.get("code-samples")
self.samples = (
[v for v in code_samples_ext if v.get("lang").lower() == "cli"]
if code_samples_ext is not None
else []
)

@property
def args(self):
"""
Expand Down
15 changes: 7 additions & 8 deletions linodecli/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,21 +304,20 @@ def configure(

if ENV_TOKEN_NAME in os.environ:
print(
f"""Using token from {ENV_TOKEN_NAME}.
Note that no token will be saved in your configuration file.
* If you lose or remove {ENV_TOKEN_NAME}.
* All profiles will use {ENV_TOKEN_NAME}."""
f"Using token from {ENV_TOKEN_NAME}.\n"
"Note that no token will be saved in your configuration file.\n"
f" * If you lose or remove {ENV_TOKEN_NAME}.\n"
f" * All profiles will use {ENV_TOKEN_NAME}."
)
username = "DEFAULT"
token = os.getenv(ENV_TOKEN_NAME)

else:
if _check_browsers() and not self.configure_with_pat:
print(
"""
The CLI will use its web-based authentication to log you in.
If you prefer to supply a Personal Access Token, use `linode-cli configure --token`.
"""
"The CLI will use its web-based authentication to log you in.\n"
"If you prefer to supply a Personal Access Token,"
"use `linode-cli configure --token`."
)
input(
"Press enter to continue. "
Expand Down
8 changes: 8 additions & 0 deletions linodecli/plugins/image-upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def call(args, context):
nargs="?",
help="A description for this Image. Blank if omitted.",
)
parser.add_argument(
"--cloud-init",
action="store_true",
help="If given, the new image will be flagged as cloud-init compatible.",
)
parser.add_argument(
"file",
metavar="FILE",
Expand Down Expand Up @@ -149,6 +154,9 @@ def call(args, context):
if parsed.description:
call_args += ["--description", parsed.description]

if parsed.cloud_init:
call_args += ["--cloud_init", "true"]

status, resp = context.client.call_operation("images", "upload", call_args)

if status != 200:
Expand Down
219 changes: 219 additions & 0 deletions linodecli/plugins/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""
This plugin allows users to access the metadata service while in a Linode.
Usage:
linode-cli metadata [ENDPOINT]
"""

import argparse
import sys

from linode_metadata import MetadataClient
from linode_metadata.objects.error import ApiError
from linode_metadata.objects.instance import ResponseBase
from requests import ConnectTimeout
from rich import print as rprint
from rich.table import Table

PLUGIN_BASE = "linode-cli metadata"


def process_sub_columns(subcolumn: ResponseBase, table: Table, values_row):
"""
Helper method to process embedded ResponseBase objects
"""
for key, value in vars(subcolumn).items():
if isinstance(value, ResponseBase):
process_sub_columns(value, table, values_row)
else:
table.add_column(key)
values_row.append(str(value))


def print_instance_table(data):
"""
Prints the table that contains information about the current instance
"""
attributes = vars(data)
values_row = []

table = Table()

for key, value in attributes.items():
if isinstance(value, ResponseBase):
process_sub_columns(value, table, values_row)
else:
table.add_column(key)
values_row.append(str(value))

table.add_row(*values_row)
rprint(table)


def print_ssh_keys_table(data):
"""
Prints the table that contains information about the SSH keys for the current instance
"""
table = Table(show_lines=True)

table.add_column("ssh keys")

if data.users.root is not None:
for key in data.users.root:
table.add_row(key)

rprint(table)


def print_networking_tables(data):
"""
Prints the table that contains information about the network of the current instance
"""
interfaces = Table(title="Interfaces", show_lines=True)

interfaces.add_column("label")
interfaces.add_column("purpose")
interfaces.add_column("ipam addresses")

for interface in data.interfaces:
attributes = vars(interface)
interface_row = []
for _, value in attributes.items():
interface_row.append(str(value))
interfaces.add_row(*interface_row)

ipv4 = Table(title="IPv4")
ipv4.add_column("ip address")
ipv4.add_column("type")
attributes = vars(data.ipv4)
for key, value in attributes.items():
for address in value:
ipv4.add_row(*[address, key])

ipv6 = Table(title="IPv6")
ipv6_data = data.ipv6
ipv6.add_column("slaac")
ipv6.add_column("link local")
ipv6.add_column("ranges")
ipv6.add_column("shared ranges")
ipv6.add_row(
*[
ipv6_data.slaac,
ipv6_data.link_local,
str(ipv6_data.ranges),
str(ipv6_data.shared_ranges),
]
)

rprint(interfaces)
rprint(ipv4)
rprint(ipv6)


def get_instance(client: MetadataClient):
"""
Get information about your instance, including plan resources
"""
data = client.get_instance()
print_instance_table(data)


def get_user_data(client: MetadataClient):
"""
Get your user data
"""
data = client.get_user_data()
rprint(data)


def get_network(client: MetadataClient):
"""
Get information about your instance’s IP addresses
"""
data = client.get_network()
print_networking_tables(data)


def get_ssh_keys(client: MetadataClient):
"""
Get information about public SSH Keys configured on your instance
"""
data = client.get_ssh_keys()
print_ssh_keys_table(data)


COMMAND_MAP = {
"instance": get_instance,
"user-data": get_user_data,
"networking": get_network,
"sshkeys": get_ssh_keys,
}


def print_help(parser: argparse.ArgumentParser):
"""
Print out the help info to the standard output
"""
parser.print_help()

# additional help
print()
print("Available endpoints: ")

command_help_map = [
[name, func.__doc__.strip()]
for name, func in sorted(COMMAND_MAP.items())
]

tab = Table(show_header=False)
for row in command_help_map:
tab.add_row(*row)
rprint(tab)


def get_metadata_parser():
"""
Builds argparser for Metadata plug-in
"""
parser = argparse.ArgumentParser(PLUGIN_BASE, add_help=False)

parser.add_argument(
"endpoint",
metavar="ENDPOINT",
nargs="?",
type=str,
help="The API endpoint to be called from the Metadata service.",
)

return parser


def call(args, _):
"""
The entrypoint for this plugin
"""

parser = get_metadata_parser()
parsed, args = parser.parse_known_args(args)

if not parsed.endpoint in COMMAND_MAP or len(args) != 0:
print_help(parser)
sys.exit(0)

# make a client, but only if we weren't printing help and endpoint is valid
if "--help" not in args:
try:
client = MetadataClient()
except ConnectTimeout as exc:
raise ConnectionError(
"Can't access Metadata service. Please verify that you are inside a Linode."
) from exc
else:
print_help(parser)
sys.exit(0)

try:
COMMAND_MAP[parsed.endpoint](client)
except ApiError as e:
sys.exit(f"Error: {e}")
6 changes: 4 additions & 2 deletions linodecli/plugins/obj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ def call(
"""
This is called when the plugin is invoked
"""
is_help = "--help" in args or "-h" in args

if not HAS_BOTO:
# we can't do anything - ask for an install
print(
Expand All @@ -540,7 +542,7 @@ def call(

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

clusters = get_available_cluster(context.client)
clusters = get_available_cluster(context.client) if not is_help else None
parser = get_obj_args_parser(clusters)
parsed, args = parser.parse_known_args(args)

Expand All @@ -556,7 +558,7 @@ def call(
secret_key = None

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

cluster = parsed.cluster
Expand Down
1 change: 0 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ requests-mock==1.11.0
boto3-stubs[s3]
build>=0.10.0
twine>=4.0.2
packaging>=23.2
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ PyYAML
packaging
rich
urllib3<3
linode-metadata
Loading

1 comment on commit 95b49eb

@zliang-akamai
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, this should be v5.48.0 instead of v5.47.0

Please sign in to comment.