-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cca895b
commit 409149d
Showing
4 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT license. | ||
|
||
""" | ||
This module wraps Azure Image Testing For Linux APIs. It depends on az CLI, no | ||
LISA or other Python dependencies. Install az CLI from here: | ||
https://learn.microsoft.com/en-us/cli/azure/install-azure-cli | ||
""" | ||
import json | ||
import logging | ||
import os | ||
import subprocess | ||
import sys | ||
import time | ||
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter | ||
from datetime import datetime | ||
from typing import Any | ||
|
||
_fmt = "%(asctime)s.%(msecs)03d[%(thread)d][%(levelname)s] %(name)s %(message)s" | ||
_datefmt = "%Y-%m-%d %H:%M:%S" | ||
_api_version = "2023-08-01-preview" | ||
|
||
_examples = """ | ||
Examples: | ||
Create a job: | ||
python -m aitl job create -s {subscription_id} -r {resource_group} -n {job_name} -b @./tier0.json | ||
List jobs: | ||
python -m aitl job list -s {subscription_id} -r {resource_group} | ||
Get a job: | ||
python -m aitl job get -s {subscription_id} -r {resource_group} -n {job_name} | ||
""" # noqa: E501 | ||
|
||
|
||
def _initialize() -> None: | ||
logging.Formatter.converter = time.gmtime | ||
logging.basicConfig(format=_fmt, datefmt=_datefmt, level=logging.INFO) | ||
|
||
|
||
def _execute(command: str, is_json: bool = False, check: bool = True) -> Any: | ||
env = os.environ.copy() | ||
process_result = subprocess.run( | ||
command, shell=True, env=env, capture_output=True, text=True, check=False | ||
) | ||
if process_result.returncode != 0: | ||
message = ( | ||
f"failed to execute command: '{command}', error: {process_result.stderr}" | ||
) | ||
if check: | ||
raise SystemExit(message) | ||
else: | ||
logging.debug(message) | ||
if is_json: | ||
result = _parse_json(process_result.stdout) | ||
else: | ||
result = process_result.stdout | ||
|
||
return result | ||
|
||
|
||
def _parse_json(content: str) -> Any: | ||
return json.loads(content) | ||
|
||
|
||
def _init_arg_parser() -> Namespace: | ||
parser = ArgumentParser( | ||
prog="aitl", epilog=_examples, formatter_class=RawTextHelpFormatter | ||
) | ||
|
||
sub_parser = parser.add_subparsers(dest="resource", required=True) | ||
_add_resource_parser(sub_parser, "job", support_update=False) | ||
_add_resource_parser(sub_parser, "template", support_update=False) | ||
|
||
return parser.parse_args() | ||
|
||
|
||
def _add_resource_parser( | ||
parser: Any, resource: str, support_update: bool = False | ||
) -> None: | ||
cmd_parser: ArgumentParser = parser.add_parser( | ||
name=resource, epilog=_examples, formatter_class=RawTextHelpFormatter | ||
) | ||
sub_parser = cmd_parser.add_subparsers(dest="action", required=True) | ||
for action in ["create", "list", "get", "delete", "update"]: | ||
if not support_update and action == "update": | ||
continue | ||
|
||
if action == "list": | ||
support_name = False | ||
else: | ||
support_name = True | ||
|
||
if action in ["get", "delete", "update"]: | ||
required_name = True | ||
else: | ||
required_name = False | ||
|
||
action_parser = sub_parser.add_parser( | ||
name=action, formatter_class=RawTextHelpFormatter | ||
) | ||
_add_common_required_parsers( | ||
action_parser, support_name=support_name, required_name=required_name | ||
) | ||
|
||
if resource == "job" and action == "create": | ||
_add_job_creation_parser(action_parser) | ||
|
||
_add_common_optional_parsers(action_parser) | ||
|
||
|
||
def _add_job_creation_parser(parser: ArgumentParser) -> None: | ||
parser.add_argument( | ||
"--body", | ||
"-b", | ||
dest="body", | ||
default="@./tier0.json", | ||
help="Request body. Use @{file} to load from a file. " | ||
"For quoting issues in different terminals, " | ||
"see https://github.com/Azure/azure-", | ||
) | ||
|
||
|
||
def _add_common_required_parsers( | ||
parser: ArgumentParser, support_name: bool = True, required_name: bool = False | ||
) -> None: | ||
parser.add_argument( | ||
"--debug", | ||
"-d", | ||
dest="debug", | ||
action="store_true", | ||
help="""Set the log level output by the console to DEBUG level. By default, the | ||
console displays logs with INFO and higher levels. The log file will | ||
contain the DEBUG level and is not affected by this setting. | ||
""", | ||
) | ||
|
||
parser.add_argument( | ||
"--subscription_id", | ||
"-s", | ||
dest="subscription_id", | ||
help="subscription id", | ||
required=True, | ||
) | ||
|
||
parser.add_argument( | ||
"--resource_group", | ||
"-r", | ||
dest="resource_group", | ||
help="resource group name", | ||
required=True, | ||
) | ||
|
||
if support_name: | ||
parser.add_argument( | ||
"--name", | ||
"-n", | ||
dest="name", | ||
help="job or job template name", | ||
required=required_name, | ||
) | ||
|
||
|
||
def _add_common_optional_parsers( | ||
parser: ArgumentParser, | ||
) -> None: | ||
parser.add_argument( | ||
"--query", | ||
"-q", | ||
dest="query", | ||
help="""JMESPath to query result. See http://jmespath.org/ for more information and examples. | ||
For example: | ||
Get job status: 'properties.provisioningState' | ||
List test results: 'properties.results[].{name:testName,status:status,message:message}' | ||
Summarize test results: 'properties.results[].status|{TOTAL:length(@),PASSED:length([?@==`"PASSED"`]),FAILED:length([?@==`"FAILED"`]),SKIPPED:length([?@==`"SKIPPED"`]),ATTEMPTED:length([?@==`"ATTEMPTED"`]),RUNNING:length([?@==`"RUNNING"`]),ASSIGNED:length([?@==`"ASSIGNED"`]),QUEUED:length([?@==`"QUEUED"`])}' | ||
""", # noqa: E501 | ||
) | ||
|
||
parser.add_argument( | ||
"--output", | ||
"-o", | ||
dest="output", | ||
help="Output format. Allowed values: json, jsonc, none, table, tsv, " | ||
"yaml, yamlc. Default: json", | ||
) | ||
|
||
parser.add_argument( | ||
"--api-version", | ||
"-v", | ||
default=_api_version, | ||
dest="api_version", | ||
help="api version", | ||
) | ||
|
||
parser.add_argument( | ||
"--provider", | ||
"-p", | ||
default="Microsoft.AzureImageTestingForLinux", | ||
dest="provider", | ||
help="provider name, internal use only", | ||
) | ||
|
||
parser.add_argument( | ||
"--endpoint", | ||
"-e", | ||
default="https://management.azure.com", | ||
dest="endpoint", | ||
help="endpoint, internal use only", | ||
) | ||
|
||
|
||
def _call_rest_api(method: str, **kwargs: Any) -> Any: | ||
subscription_id = kwargs.pop("subscription_id") | ||
resource_group = kwargs.pop("resource_group") | ||
provider = kwargs.pop("provider") | ||
name = kwargs.pop("name", "") | ||
endpoint = kwargs.pop("endpoint") | ||
api_version = kwargs.pop("api_version") | ||
body = kwargs.pop("body", "") | ||
resource_type = kwargs.pop("resource") | ||
query = kwargs.pop("query", "") | ||
output = kwargs.pop("output", "") | ||
|
||
if resource_type == "job": | ||
resource_type = "jobs" | ||
else: | ||
resource_type = "jobTemplates" | ||
|
||
resource_url = ( | ||
f"{endpoint}/subscriptions/{subscription_id}/resourceGroups/{resource_group}" | ||
f"/providers/{provider}/{resource_type}" | ||
) | ||
if name: | ||
resource_url = f"{resource_url}/{name}" | ||
resource_url = f"{resource_url}?api-version={api_version}" | ||
|
||
command = f"az rest --method {method} --uri {resource_url}" | ||
if body: | ||
command = f'{command} --body "{body}" --headers "Content-Type=application/json"' | ||
if query: | ||
command = f'{command} --query "{query}"' | ||
if output: | ||
command = f"{command} --output {output}" | ||
|
||
logging.info(f"calling REST API: {resource_url}") | ||
result = _execute(command=command) | ||
logging.info(f"called {resource_type} {action} finished.") | ||
|
||
if result: | ||
print() | ||
print(result) | ||
else: | ||
logging.info("no result returned, please check later.") | ||
|
||
return result | ||
|
||
|
||
def _process_create_job(**kwargs: Any) -> Any: | ||
name: str = kwargs.get("name", "") | ||
if not name: | ||
name = datetime.utcnow().strftime("aitl_%Y%m%d_%H%M%S_%f")[:-3] | ||
logging.info(f"job name is not specified, generated job name: '{name}'.") | ||
kwargs["name"] = name | ||
|
||
return kwargs | ||
|
||
|
||
if __name__ == "__main__": | ||
_initialize() | ||
|
||
cmd_args = _init_arg_parser() | ||
if cmd_args.debug: | ||
logging.getLogger().setLevel(logging.DEBUG) | ||
|
||
result = _execute("az account show", check=False) | ||
if not result: | ||
logging.info("not logged in, calling 'az login'...") | ||
_execute("az login") | ||
|
||
logging.debug(f"starting command with args: {cmd_args}") | ||
|
||
kwargs = vars(cmd_args) | ||
action = kwargs.pop("action") | ||
resource = kwargs.get("resource") | ||
|
||
if action == "create": | ||
http_method = kwargs.pop("method", "PUT") | ||
elif action == "update": | ||
http_method = kwargs.pop("method", "POST") | ||
elif action == "delete": | ||
http_method = kwargs.pop("method", "DELETE") | ||
else: | ||
http_method = kwargs.pop("method", "GET") | ||
|
||
method_name = f"_process_{action}_{resource}" | ||
self = sys.modules[__name__] | ||
if hasattr(self, method_name): | ||
logging.debug(f"calling {method_name}...") | ||
kwargs = getattr(self, method_name)(**kwargs) | ||
|
||
_call_rest_api(method=http_method, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"location": "westus3", | ||
"properties": { | ||
"jobTemplateInstance": { | ||
"templateTags": [], | ||
"selections": [ | ||
{ | ||
"casePriority": [ | ||
0 | ||
] | ||
} | ||
], | ||
"region": [], | ||
"vmSize": [] | ||
}, | ||
"image": { | ||
"type": "marketplace", | ||
"offer": "0001-com-ubuntu-server-focal", | ||
"publisher": "Canonical", | ||
"sku": "20_04-lts-gen2", | ||
"version": "latest", | ||
"architecture": "x64", | ||
"vhdGeneration": 2 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"location": "westus3", | ||
"properties": { | ||
"jobTemplateInstance": { | ||
"templateTags": [], | ||
"selections": [ | ||
{ | ||
"casePriority": [ | ||
0, | ||
1 | ||
] | ||
} | ||
], | ||
"region": [], | ||
"vmSize": [], | ||
"concurrency": 4 | ||
}, | ||
"image": { | ||
"type": "marketplace", | ||
"offer": "0001-com-ubuntu-server-focal", | ||
"publisher": "Canonical", | ||
"sku": "20_04-lts-gen2", | ||
"version": "latest", | ||
"architecture": "x64", | ||
"vhdGeneration": 2 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"location": "westus3", | ||
"properties": { | ||
"jobTemplateInstance": { | ||
"templateTags": [], | ||
"selections": [ | ||
{ | ||
"casePriority": [ | ||
0, | ||
1, | ||
2 | ||
] | ||
} | ||
], | ||
"region": [], | ||
"vmSize": [], | ||
"concurrency": 4 | ||
}, | ||
"image": { | ||
"type": "marketplace", | ||
"offer": "0001-com-ubuntu-server-focal", | ||
"publisher": "Canonical", | ||
"sku": "20_04-lts-gen2", | ||
"version": "latest", | ||
"architecture": "x64", | ||
"vhdGeneration": 2 | ||
} | ||
} | ||
} |