diff --git a/.travis.yml b/.travis.yml index 1d36998..8b26b5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ script: - pylint src - export - ./setup.py bdist_wheel -- ./publish.sh \ No newline at end of file +- ./scripts/publish.sh \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..2d8697a --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,14 @@ +Micrsoft ADX A01 Automation System CLI +====================================== + + +.. :changelog: + +Release History +=============== + +0.14 +++++ + +* New Features: Allow specify --last and --skip while query runs to limit the return data set. +* New Features: Allow --me option while query runs to list runs belong to the current user only. \ No newline at end of file diff --git a/publish.sh b/scripts/publish.sh similarity index 70% rename from publish.sh rename to scripts/publish.sh index 0f40d5d..1bb6c19 100755 --- a/publish.sh +++ b/scripts/publish.sh @@ -13,4 +13,5 @@ version=${version/-py3-none-any.whl/} echo $version az storage blob upload -c client -f $wheel_file -n archive/$wheel_file --validate-content --no-progress -az storage blob url -c client -n archive/$wheel_file -otsv | az storage blob upload -c client -f /dev/stdin -n latest --validate-content --no-progress +az storage blob url -c client -n archive/$wheel_file -otsv | tee ./blob_path +az storage blob upload -c client -f ./blob_path -n latest --validate-content --no-progress diff --git a/setup.py b/setup.py index 924d37b..66a5a36 100755 --- a/setup.py +++ b/setup.py @@ -1,20 +1,27 @@ #!/usr/bin/env python3 import os +import sys import datetime from setuptools import setup -def get_version() -> str: - if "TRAVIS" in os.environ: - tag = os.environ.get('TRAVIS_TAG', None) - if tag: - return tag - else: - return f'0.0.0.{os.environ["TRAVIS_BUILD_NUMBER"]}' - return f'0.0.0.{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}' +ROOT_INIT = os.path.join(os.path.dirname(__file__), 'src', 'a01', '__init__.py') +VERSION = os.environ.get('TRAVIS_TAG') +try: + if VERSION: + with open(ROOT_INIT, 'w') as file_handler: + file_handler.write(f'__version__ = {VERSION}') + else: + with open(ROOT_INIT, 'r') as file_handler: + line = file_handler.readline() + VERSION = line.split('=')[1].strip() +except (ValueError, IOError): + print('Fail to pass version string.', file=sys.stderr, flush=True) + sys.exit(1) -VERSION = get_version() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() CLASSIFIERS = [ 'Development Status :: 4 - Beta', @@ -40,7 +47,7 @@ setup( name='adx-automation-cli', version=VERSION, description='ADX Automation CLI', - long_description='Command line tools for ADX automation system', + long_description=HISTORY, license='MIT', author='Microsoft Corporation', author_email='trdai@microsoft.com', diff --git a/src/a01/__init__.py b/src/a01/__init__.py index e69de29..6af95d8 100644 --- a/src/a01/__init__.py +++ b/src/a01/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0.0+local' diff --git a/src/a01/__main__.py b/src/a01/__main__.py index a4f210e..e956f95 100644 --- a/src/a01/__main__.py +++ b/src/a01/__main__.py @@ -1,3 +1,11 @@ +from a01 import __version__ +from a01.cli import cmd + +@cmd('version', desc='Print version information') +def version() -> None: + print(__version__) + + def main() -> None: from a01.cli import setup_commands diff --git a/src/a01/auth.py b/src/a01/auth.py index 11b8767..5da3b38 100644 --- a/src/a01/auth.py +++ b/src/a01/auth.py @@ -141,6 +141,22 @@ class AuthSettings(object): self.logger.exception(f'Fail to save the file {TOKEN_FILE}') raise + def get_user_name(self) -> str: + """Returns the name of current user. For a user account, it returns its user id. For a service principal, it + returns its service principal ID.""" + if not self.has_login: + print('You need to login. Usage: a01 login.') + sys.exit(1) + + try: + return self.user_id + except AuthenticationError: + try: + return self.service_principal_id + except AuthenticationError: + print("Fail to find user name. Tried both user id and service principal.", file=sys.stderr) + sys.exit(1) + @staticmethod def _get_auth_context() -> adal.AuthenticationContext: return adal.AuthenticationContext(AUTHORITY_URL, api_version=None) @@ -209,18 +225,3 @@ def whoami() -> None: print(AuthSettings().summary) except AuthenticationError: print('You need to login. Usage: a01 login.') - - -def get_user_id() -> str: - try: - return AuthSettings().user_id - except AuthenticationError: - print('You need to login. Usage: a01 login.') - sys.exit(1) - -def get_service_principal_id() -> str: - try: - return AuthSettings().service_principal_id - except AuthenticationError: - print('You need to login. Usage: a01 login.') - sys.exit(1) diff --git a/src/a01/common.py b/src/a01/common.py index a06197b..9063ff4 100644 --- a/src/a01/common.py +++ b/src/a01/common.py @@ -66,3 +66,4 @@ class A01Config(configparser.ConfigParser): # pylint: disable=too-many-ancestor @property def endpoint_uri(self) -> str: return f'https://{self.endpoint}/api' + # return 'http://127.0.0.1:5000/api' diff --git a/src/a01/models/run.py b/src/a01/models/run.py index 4f45615..90d49e9 100644 --- a/src/a01/models/run.py +++ b/src/a01/models/run.py @@ -1,6 +1,7 @@ import json import datetime import base64 +import urllib from typing import List, Tuple, Generator import colorama @@ -15,10 +16,11 @@ from a01.common import get_logger, A01Config, NAMESPACE class Run(object): logger = get_logger('Run') - def __init__(self, name: str, settings: dict, details: dict): + def __init__(self, name: str, settings: dict, details: dict, owner: str = None): self.name = name self.settings = settings self.details = details + self.owner = owner self.id = None # pylint: disable=invalid-name self.creation = None @@ -27,7 +29,8 @@ class Run(object): result = { 'name': self.name, 'settings': self.settings, - 'details': self.details + 'details': self.details, + 'owner': self.owner } return result @@ -71,6 +74,7 @@ class Run(object): result = Run(name=data['name'], settings=data['settings'], details=data['details']) result.id = data['id'] result.creation = datetime.datetime.strptime(data['creation'], '%Y-%m-%dT%H:%M:%SZ') + result.owner = data.get('owner', None) return result @@ -91,7 +95,7 @@ class RunCollection(object): for run in self.runs: time = (run.creation - datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M PST') remark = run.details.get('remark', None) or run.settings.get('a01.reserved.remark', '') - owner = run.details.get('creator', None) or run.details.get('a01.reserved.creator', '') + owner = run.owner or run.details.get('creator', None) or run.details.get('a01.reserved.creator', '') row = [run.id, run.name, time, remark, owner] if remark and remark.lower() == 'official': @@ -105,13 +109,22 @@ class RunCollection(object): return 'Id', 'Name', 'Creation', 'Remark', 'Owner' @classmethod - def get(cls) -> 'RunCollection': + def get(cls, **kwargs) -> 'RunCollection': try: - resp = session.get(f'{cls.endpoint_uri()}/runs') + url = f'{cls.endpoint_uri()}/runs' + query = {} + for key, value in kwargs.items(): + if value is not None: + query[key] = value + + if query: + url = f'{url}?{urllib.parse.urlencode(query)}' + + resp = session.get(url) resp.raise_for_status() runs = [Run.from_dict(each) for each in resp.json()] - runs = sorted(runs, key=lambda r: r.id) + runs = sorted(runs, key=lambda r: r.id, reverse=True) return RunCollection(runs) except HTTPError: diff --git a/src/a01/runs.py b/src/a01/runs.py index 7e02ea4..c8e94b6 100644 --- a/src/a01/runs.py +++ b/src/a01/runs.py @@ -20,11 +20,12 @@ from kubernetes.client.models.v1_env_var import V1EnvVar from kubernetes.client.models.v1_env_var_source import V1EnvVarSource from kubernetes.client.models.v1_secret_key_selector import V1SecretKeySelector +import a01 import a01.models from a01.common import get_logger, A01Config, COMMON_IMAGE_PULL_SECRET from a01.cli import cmd, arg from a01.communication import session -from a01.auth import get_user_id, get_service_principal_id +from a01.auth import AuthSettings, AuthenticationError from a01.output import output_in_table logger = get_logger(__name__) # pylint: disable=invalid-name @@ -33,13 +34,27 @@ logger = get_logger(__name__) # pylint: disable=invalid-name @cmd('get runs', desc='Retrieve the runs.') -def get_runs() -> None: +@arg('owner', help='Query runs by owner.') +@arg('me', help='Query runs created by me.') +@arg('last', help='Returns the last NUMBER of records. Default: 20.') +@arg('skip', help='Returns the records after skipping given number of records at the bottom. Default: 0.') +def get_runs(me: bool = False, last: int = 20, skip: int = 0, owner: str = None) -> None: # pylint: disable=invalid-name try: - runs = a01.models.RunCollection.get() + if me and owner: + raise ValueError('--me and --user are mutually exclusive.') + elif me: + owner = AuthSettings().get_user_name() + + runs = a01.models.RunCollection.get(owner=owner, last=last, skip=skip) output_in_table(runs.get_table_view(), headers=runs.get_table_header()) except ValueError as err: logger.error(err) sys.exit(1) + except AuthenticationError as err: + logger.error(err) + print('You need to login. Usage: a01 login.', file=sys.stderr) + sys.exit(1) + @cmd('get run', desc='Retrieve a run') @@ -106,8 +121,9 @@ def get_run(run_id: str, log: bool = False, recording: bool = False, recording_a def create_run(image: str, from_failures: str = None, live: bool = False, parallelism: int = 3, query: str = None, remark: str = '', email: bool = False, secret: str = None, mode: str = None, reset_run: str = None) -> None: + auth = AuthSettings() remark = remark or '' - creator = get_user_id() if email else get_service_principal_id() + creator = auth.get_user_name() try: if not reset_run: @@ -119,7 +135,7 @@ def create_run(image: str, from_failures: str = None, live: bool = False, parall 'a01.reserved.storageshare': 'k8slog', 'a01.reserved.testquery': query, 'a01.reserved.remark': remark, - 'a01.reserved.useremail': get_user_id() if email else '', + 'a01.reserved.useremail': auth.user_id if email else '', 'a01.reserved.initparallelism': parallelism, 'a01.reserved.livemode': str(live), 'a01.reserved.testmode': mode, @@ -127,8 +143,9 @@ def create_run(image: str, from_failures: str = None, live: bool = False, parall }, details={ 'a01.reserved.creator': creator, - 'a01.reserved.client': 'A01 CLI' - }) + 'a01.reserved.client': f'CLI {a01.__version__}' + }, + owner=creator) # prune to_delete = [k for k, v in run_model.settings.items() if not v]