diff --git a/.gitignore b/.gitignore index 1e835aa..bc1cc8a 100755 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .vscode __pycache__ -ucloud_cli.egg-info +uncloud_cli.egg-info build/ -dist/ \ No newline at end of file +dist/ diff --git a/bin/ucloud-cli b/bin/uncloud-cli similarity index 79% rename from bin/ucloud-cli rename to bin/uncloud-cli index 2b27f14..5d7c4b0 100755 --- a/bin/ucloud-cli +++ b/bin/uncloud-cli @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import click -import sys from uncloud_cli.commands.vm import vm from uncloud_cli.commands.user import user @@ -9,16 +8,13 @@ from uncloud_cli.commands.host import host from uncloud_cli.commands.image import image from uncloud_cli.commands.network import network -from uncloud_cli.helper import exception_handler - @click.group() def entry_point(): pass -if __name__ == "__main__": - sys.excepthook = exception_handler +if __name__ == '__main__': entry_point.add_command(vm) entry_point.add_command(user) entry_point.add_command(image) diff --git a/setup.py b/setup.py index 497cea1..ee40e77 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,12 @@ -import os - from setuptools import setup, find_packages -with open("README.md", "r") as fh: +with open('README.md', 'r') as fh: long_description = fh.read() -setup(name='ucloud_cli', +setup(name='uncloud_cli', version='0.1', - description='A utility to interact with ucloud server.', - url='https://code.ungleich.ch/ucloud/ucloud-cli', + description='A utility to interact with uncloud server.', + url='https://code.ungleich.ch/uncloud/uncloud-cli', long_description=long_description, long_description_content_type='text/markdown', classifiers=[ @@ -26,6 +24,5 @@ setup(name='ucloud_cli', 'pyotp', 'click' ], - scripts=['bin/ucloud-cli'], - data_files=[(os.path.expanduser('~/ucloud/'), ['conf/ucloud-cli.conf'])], + scripts=['bin/uncloud-cli'], zip_safe=False) diff --git a/uncloud_cli/commands/helper.py b/uncloud_cli/commands/helper.py index ce3da6b..c7661c9 100755 --- a/uncloud_cli/commands/helper.py +++ b/uncloud_cli/commands/helper.py @@ -1,20 +1,71 @@ import json +import binascii +import click +import requests + +from os.path import join as join_path from pyotp import TOTP - - -class OTPCredentials: - def __init__(self, name, realm, seed): - self.name = name # type: str - self.realm = realm # type: str - self.seed = seed # type: str - - def get_json(self): - return {"name": self.name, "realm": self.realm, "token": TOTP(self.seed).now()} +from uncloud_cli.config import config, config_file def load_dump_pretty(content): if isinstance(content, bytes): - content = content.decode("utf-8") + content = content.decode('utf-8') parsed = json.loads(content) return json.dumps(parsed, indent=4, sort_keys=True) + + +def make_request(*args, data=None, request_method=requests.post): + r = request_method( + join_path(config.get('client', 'api_server'), *args), json=data + ) + try: + print(load_dump_pretty(r.content)) + except Exception: + print('Error occurred while getting output from api server.') + + +def get_token(ctx, param, value): + if value is not None: + try: + token = TOTP(value).now() + except binascii.Error: + raise click.BadParameter('') + else: + param.name = 'token' + return token + + +def add_otp_options(f): + options = [ + click.option( + '--name', required=True, default=config.get('client', 'name', fallback=''), + show_default='name mentioned in {}'.format(config_file), prompt=True + ), + click.option( + '--realm', required=True, default=config.get('client', 'realm', fallback=''), + show_default='realm mentioned in {}'.format(config_file), prompt=True + ), + click.option( + '--seed', required=True, default=config.get('client', 'seed', fallback=''), + callback=get_token, prompt=True, + show_default='seed mentioned in {}'.format(config_file) + ) + ] + + for opt in reversed(options): + f = opt(f) + + return f + + +def add_vm_options(f): + options = [ + click.option('--vm-name', required=True), + click.option('--action', required=True, default=f.__name__) + ] + for opt in reversed(options): + f = opt(f) + + return f diff --git a/uncloud_cli/commands/host.py b/uncloud_cli/commands/host.py index c0a04f3..29ee417 100755 --- a/uncloud_cli/commands/host.py +++ b/uncloud_cli/commands/host.py @@ -1,9 +1,7 @@ import click import requests -from .helper import OTPCredentials, load_dump_pretty -from uncloud_cli.config import env_vars -from os.path import join as join_path +from .helper import add_otp_options, make_request @click.group() @@ -11,28 +9,23 @@ def host(): pass -@host.command("create") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--hostname", required=True) -@click.option("--cpu", required=True, type=int) -@click.option("--ram", required=True) -@click.option("--os-ssd", required=True) -@click.option("--hdd", default=list(), multiple=True) -def create(name, realm, seed, hostname, cpu, ram, os_ssd, hdd): - data = { - **OTPCredentials(name, realm, seed).get_json(), - "hostname": hostname, - "specs": {"cpu": cpu, "ram": ram, "os-ssd": os_ssd, "hdd": hdd}, +@host.command('create') +@add_otp_options +@click.option('--hostname', required=True) +@click.option('--cpu', required=True, type=int) +@click.option('--ram', required=True) +@click.option('--os-ssd', required=True) +@click.option('--hdd', default=list(), multiple=True) +def create(**kwargs): + kwargs['specs'] = { + 'cpu': kwargs.pop('cpu'), + 'ram': kwargs.pop('ram'), + 'os-ssd': kwargs.pop('os_ssd'), + 'hdd': kwargs.pop('hdd') } - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "host", "create"), json=data - ) - print(load_dump_pretty(r.content)) + make_request('host', 'create', data=kwargs) -@host.command("list") +@host.command('list') def list_host(): - r = requests.get(join_path(env_vars.get("UCLOUD_API_SERVER"), "host", "list")) - print(load_dump_pretty(r.content)) + make_request('host', 'list', request_method=requests.get) diff --git a/uncloud_cli/commands/image.py b/uncloud_cli/commands/image.py index 18e9cf3..916d276 100755 --- a/uncloud_cli/commands/image.py +++ b/uncloud_cli/commands/image.py @@ -1,31 +1,24 @@ -from uncloud_cli.commands.helper import load_dump_pretty -from uncloud_cli.config import env_vars -from os.path import join as join_path - import click import requests +from uncloud_cli.commands.helper import make_request + @click.group() def image(): pass -@image.command("list") -@click.option("--public", is_flag=True) +@image.command('list') +@click.option('--public', is_flag=True) def _list(public): if public: - r = requests.get(join_path(env_vars.get("UCLOUD_API_SERVER"), "image", "list-public")) - print(load_dump_pretty(r.content)) + make_request('image', 'list-public', request_method=requests.get) -@image.command("create-from-file") -@click.option("--name", required=True) -@click.option("--uuid", required=True) -@click.option("--image-store-name", required=True) -def create_from_file(name, uuid, image_store_name): - data = {"name": name, "uuid": uuid, "image_store": image_store_name} - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "image", "create"), json=data - ) - print(load_dump_pretty(r.content)) +@image.command('create-from-file') +@click.option('--name', required=True) +@click.option('--uuid', required=True) +@click.option('--image-store-name', 'image_store', required=True) +def create_from_file(**kwargs): + make_request('image', 'create', data=kwargs) diff --git a/uncloud_cli/commands/network.py b/uncloud_cli/commands/network.py index 48cbc0d..978ec75 100644 --- a/uncloud_cli/commands/network.py +++ b/uncloud_cli/commands/network.py @@ -1,6 +1,4 @@ -from uncloud_cli.commands.helper import load_dump_pretty, OTPCredentials -from uncloud_cli.config import env_vars -from os.path import join as join_path +from uncloud_cli.commands.helper import add_otp_options, make_request import click import requests @@ -11,21 +9,10 @@ def network(): pass -@network.command("create") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--network-name", required=True) -@click.option("--network-type", required=True) -@click.option("--user", required=True, type=bool, default=False) -def create(name, realm, seed, network_name, network_type, user): - data = { - **OTPCredentials(name, realm, seed).get_json(), - "network_name": network_name, - "type": network_type, - "user": user, - } - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "network", "create"), json=data - ) - print(load_dump_pretty(r.content)) +@network.command('create') +@add_otp_options +@click.option('--network-name', required=True) +@click.option('--network-type', 'type', required=True) +@click.option('--user', required=True, type=bool, default=False) +def create(**kwargs): + make_request('network', 'create', data=kwargs) diff --git a/uncloud_cli/commands/user.py b/uncloud_cli/commands/user.py index d8957be..fbfdee2 100755 --- a/uncloud_cli/commands/user.py +++ b/uncloud_cli/commands/user.py @@ -1,9 +1,5 @@ -from uncloud_cli.commands.helper import OTPCredentials, load_dump_pretty -from uncloud_cli.config import env_vars -from os.path import join as join_path - import click -import requests +from uncloud_cli.commands.helper import add_otp_options, make_request @click.group() @@ -11,80 +7,41 @@ def user(): pass -@user.command("files") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -def list_files(name, realm, seed): - data = OTPCredentials(name, realm, seed).get_json() - r = requests.get( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "files"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('files') +@add_otp_options +def list_files(**kwargs): + make_request('user', 'files', data=kwargs) -@user.command("vms") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -def list_vms(name, realm, seed): - data = OTPCredentials(name, realm, seed).get_json() - r = requests.get( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "vms"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('vms') +@add_otp_options +def list_vms(**kwargs): + make_request('user', 'vms', data=kwargs) -@user.command("networks") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -def list_networks(name, realm, seed): - data = OTPCredentials(name, realm, seed).get_json() - r = requests.get( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "networks"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('networks') +@add_otp_options +def list_networks(**kwargs): + make_request('user', 'network', data=kwargs) -@user.command("add-ssh") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--key-name", required=True) -@click.option("--key", required=True) -def add_ssh(name, realm, seed, key_name, key): - otp = OTPCredentials(name, realm, seed) - data = {**otp.get_json(), "key_name": key_name, "key": key} - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "add-ssh"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('add-ssh') +@add_otp_options +@click.option('--key-name', required=True) +@click.option('--key', required=True) +def add_ssh(**kwargs): + make_request('user', 'add-ssh', data=kwargs) -@user.command("remove-ssh") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--key-name", required=True) -def remove_ssh(name, realm, seed, key_name): - otp = OTPCredentials(name, realm, seed) - data = {**otp.get_json(), "key_name": key_name} - r = requests.get( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "remove-ssh"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('remove-ssh') +@add_otp_options +@click.option('--key-name', required=True) +def remove_ssh(**kwargs): + make_request('user', 'remove-ssh', data=kwargs) -@user.command("get-ssh") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--key-name", default="") -def get_ssh(name, realm, seed, key_name): - otp = OTPCredentials(name, realm, seed) - data = {**otp.get_json(), "key_name": key_name} - r = requests.get( - join_path(env_vars.get("UCLOUD_API_SERVER"), "user", "get-ssh"), json=data - ) - print(load_dump_pretty(r.content)) +@user.command('get-ssh') +@add_otp_options +@click.option('--key-name', default='') +def get_ssh(**kwargs): + make_request('user', 'get-ssh', data=kwargs) diff --git a/uncloud_cli/commands/vm.py b/uncloud_cli/commands/vm.py index 3c5c90c..c8616b2 100755 --- a/uncloud_cli/commands/vm.py +++ b/uncloud_cli/commands/vm.py @@ -1,19 +1,6 @@ import click -import json -import requests -import subprocess as sp -from uncloud_cli.commands.helper import OTPCredentials, load_dump_pretty -from uncloud_cli.config import env_vars -from os.path import join as join_path - - -def vm_command(command, otp, vm_name, **kwargs): - data = {**otp.get_json(), "vm_name": vm_name, "action": command, **kwargs} - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "vm", "action"), json=data - ) - return r +from uncloud_cli.commands.helper import add_otp_options, make_request, add_vm_options @click.group() @@ -21,121 +8,56 @@ def vm(): pass -@vm.command("create") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--cpu", required=True, type=int) -@click.option("--ram", required=True) -@click.option("--os-ssd", required=True) -@click.option("--hdd", default=list(), multiple=True) -@click.option("--image", required=True) -@click.option("--network", default=list(), multiple=True) -def create(name, realm, seed, vm_name, cpu, ram, os_ssd, hdd, image, network): - data = { - **OTPCredentials(name, realm, seed).get_json(), - "vm_name": vm_name, - "specs": {"cpu": cpu, "ram": ram, "os-ssd": os_ssd, "hdd": hdd}, - "network": network, - "image": image, +@vm.command('create') +@add_otp_options +@add_vm_options +@click.option('--cpu', required=True, type=int) +@click.option('--ram', required=True) +@click.option('--os-ssd', required=True) +@click.option('--hdd', default=list(), multiple=True) +@click.option('--image', required=True) +@click.option('--network', default=list(), multiple=True) +def create(**kwargs): + kwargs['specs'] = { + 'cpu': kwargs.pop('cpu'), + 'ram': kwargs.pop('ram'), + 'os-ssd': kwargs.pop('os_ssd'), + 'hdd': kwargs.pop('hdd') } - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "vm", "create"), json=data - ) - print(load_dump_pretty(r.content)) + make_request('vm', kwargs.pop('action'), data=kwargs) -@vm.command("start") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--in_support_of") -def start(name, realm, seed, vm_name, in_support_of): - r = vm_command( - "start", OTPCredentials(name, realm, seed), vm_name, in_support_of=in_support_of - ) - print(load_dump_pretty(r.content)) +@vm.command('start') +@add_otp_options +@add_vm_options +def start(**kwargs): + make_request('vm', 'action', data=kwargs) -@vm.command("stop") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--in_support_of") -def stop(name, realm, seed, vm_name, in_support_of): - r = vm_command( - "stop", OTPCredentials(name, realm, seed), vm_name, in_support_of=in_support_of - ) - print(load_dump_pretty(r.content)) +@vm.command('stop') +@add_otp_options +@add_vm_options +def stop(**kwargs): + make_request('vm', 'action', data=kwargs) -@vm.command("delete") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--in_support_of") -def delete(name, realm, seed, vm_name, in_support_of): - r = vm_command( - "delete", - OTPCredentials(name, realm, seed), - vm_name, - in_support_of=in_support_of, - ) - print(load_dump_pretty(r.content)) +@vm.command('delete') +@add_otp_options +@add_vm_options +def delete(**kwargs): + make_request('vm', 'action', data=kwargs) -@vm.command("status") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--in_support_of") -def status(name, realm, seed, vm_name, in_support_of): - otp = OTPCredentials(name, realm, seed) - data = {**otp.get_json(), "vm_name": vm_name, "in_support_of": in_support_of} - r = requests.get(join_path(env_vars.get("UCLOUD_API_SERVER"), "vm", "status"), json=data) - print(load_dump_pretty(r.content)) +@vm.command('status') +@add_otp_options +@click.option('--vm-name', required=True) +def status(**kwargs): + make_request('vm', 'status', data=kwargs) -@vm.command("migrate") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--destination", required=True) -@click.option("--in_support_of") -def vm_migration(name, realm, seed, vm_name, destination, in_support_of): - otp = OTPCredentials(name, realm, seed) - data = { - **otp.get_json(), - "vm_name": vm_name, - "destination": destination, - "in_support_of": in_support_of, - } - r = requests.post( - join_path(env_vars.get("UCLOUD_API_SERVER"), "vm", "migrate"), json=data - ) - print(load_dump_pretty(r.content)) - - -@vm.command("ssh") -@click.option("--name", required=True, default=env_vars.get("OTP_NAME")) -@click.option("--realm", required=True, default=env_vars.get("OTP_REALM")) -@click.option("--seed", required=True, default=env_vars.get("OTP_SEED")) -@click.option("--vm-name", required=True) -@click.option("--in_support_of") -def ssh(name, realm, seed, vm_name, in_support_of): - otp = OTPCredentials(name, realm, seed) - data = {**otp.get_json(), "vm_name": vm_name, "in_support_of": in_support_of} - r = requests.get(join_path(env_vars.get("UCLOUD_API_SERVER"), "vm", "status"), json=data) - try: - _json = json.loads(r.content) - sp.run(['ssh', '-o', 'ConnectTimeout=10', - 'root@{}'.format(_json['ip'][0])]) - except Exception as err: - print("Some error occurred while accessing VM." - "Make sure VM is running", err) +@vm.command('migrate') +@add_otp_options +@click.option('--vm-name', required=True) +@click.option('--destination', required=True) +def vm_migration(**kwargs): + make_request('vm', 'migrate', data=kwargs) diff --git a/uncloud_cli/config.py b/uncloud_cli/config.py index 45343d8..9af3ba9 100644 --- a/uncloud_cli/config.py +++ b/uncloud_cli/config.py @@ -1,9 +1,10 @@ import sys -from os.path import expanduser -from decouple import Config, RepositoryEnv +import configparser +import os +config_file = os.path.expanduser('~/uncloud/uncloud.conf') try: - env_vars = Config(RepositoryEnv(expanduser("~/uncloud/uncloud-cli.conf"))) + config = configparser.ConfigParser() + config.read(config_file) except Exception as err: - print(err) - sys.exit(1) \ No newline at end of file + sys.exit(err) diff --git a/uncloud_cli/helper.py b/uncloud_cli/helper.py deleted file mode 100644 index e029945..0000000 --- a/uncloud_cli/helper.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -import sys - - -def exception_handler(exception_type, exception, traceback): - if bool(os.getenv('DEBUG_UCLOUD')) is True: - sys.__excepthook__(exception_type, exception, traceback) - else: - print("%s: %s" % (exception_type.__name__, exception))