Compare commits

..

46 Commits

Author SHA1 Message Date
aea65450b9 Added os list 2022-07-25 02:14:41 +03:00
975dab4bc5 Added plan list 2022-07-25 01:57:10 +03:00
1436a8d777 Added vds creation 2022-07-22 17:56:25 +03:00
9cb19cf4b5 Minor 2022-07-22 17:12:18 +03:00
efb2fc09ee At least it works 2022-07-22 17:11:25 +03:00
79dbcbd943 Sample vds creation 2022-07-22 03:51:53 +03:00
b2872e92d9 Added '--raw' option to vds remove 2022-07-22 03:01:21 +03:00
288ccd7357 Added '--raw' option to vds clone 2022-07-10 04:17:27 +03:00
94953ccf16 Added '--raw' option to vds stop 2022-07-10 04:15:55 +03:00
2446a74b76 Added '--raw' option to vds start 2022-07-10 04:13:54 +03:00
e84f13161a Added '--raw' option to dbs list 2022-07-10 04:11:24 +03:00
9435a46fbd Added '--raw' option to dbs creation 2022-07-10 03:57:59 +03:00
082db4e62e Ура, мы меняем ключевые особенности авторизации не оповещая клиентов. 2022-07-10 03:52:42 +03:00
d24a5397d1 README.md 2022-07-10 03:40:25 +03:00
8da2afc09d Added DB creation [2] 2022-04-27 17:48:04 +03:00
85f726e84c Added DB creation 2022-04-27 17:45:45 +03:00
731b8c5cd4 implemented dbs goto 2022-04-23 11:58:36 +03:00
a08e62e619 Implemented vds-goto 2022-04-22 18:54:37 +03:00
8bb993c59d Help and Dbaas.get() function 2022-04-22 18:39:34 +03:00
50f2dd02b6 Readable DB types 2022-04-22 18:21:53 +03:00
2e66073479 DBaaS Listing 2022-04-21 21:32:52 +03:00
c23cb55324 More help 2022-04-21 21:11:02 +03:00
3b5479d571 Now you can run it in virtualenv (forgot "requests" library) 2022-04-21 21:01:29 +03:00
b35c292e16 Snapshots 2022-04-10 03:43:24 +03:00
65df688006 backup remove 2022-04-10 02:49:27 +03:00
f9ac1a0850 Merge branch 'master' of ssh://git.lulzette.ru:3111/lulzette/twvdscli 2022-04-10 02:35:51 +03:00
e18779dc20 backup list 2022-04-10 02:34:26 +03:00
3bab5650a6 LICENSE 2022-04-09 11:45:23 +03:00
a120b520e5 Ya ne umeu v git revert 2022-04-09 11:26:04 +03:00
56780a615c Some PEP-8 2022-04-09 00:44:59 +03:00
202ab2e2d3 Fixed 2022-04-09 00:43:00 +03:00
849a74908d Revert "Add setup.py"
This reverts commit ff8eaad69a.
2022-04-09 00:39:34 +03:00
1ch
773f9caed3 Merge pull request #1 from gechandesu/setup
Setup
2022-04-08 21:43:22 +03:00
ge
5f54e3817e Update README 2022-04-08 21:31:59 +03:00
ge
f4ba7e9803 Add main() function 2022-04-08 21:27:45 +03:00
ge
ff8eaad69a Add setup.py 2022-04-08 21:08:05 +03:00
eeeded888f requirements.txt and README.md 2022-04-08 15:20:33 +03:00
6e203a5b5a VDS Removing 2022-04-07 23:46:57 +03:00
cd950292ac Bugfix 2022-04-07 23:31:50 +03:00
bc3510b937 Cloning 2022-04-07 23:31:30 +03:00
ceeddaca7d Comments! 2022-04-07 18:10:44 +03:00
f7efa801fa Preetify start()/stop() frontend functions 2022-04-07 17:21:11 +03:00
1b4f4b8484 Changed required argument to optional in stop() start() 2022-04-07 17:05:19 +03:00
d9f951f316 replace ugly tables with PrettyTables [vds_list()] 2022-04-07 16:11:06 +03:00
6df227e7e6 replace ugly tables with PrettyTables 2022-04-07 16:08:28 +03:00
ca25bbf388 Fixed access_token 2022-04-07 16:00:14 +03:00
5 changed files with 810 additions and 28 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
__pycache__
venv

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
Copyright 2022 ivan "at" lulzette.ru
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
twvdscli
====
Эта утилита предназначена для управления серверами и сервисами в Timeweb Cloud.
Функциональность:
## VDS
* Создание
* Удаление
* Клонирование
* Получение списка
* Получение информации по конкретной VDS
### Снапшоты
* Создание
* Откат
* Удаление
### Бекапы
* Создание
* Вывод списка
* Удаление
P.S.: Больше возможностей нет из-за ограниченности API
## DBaaS
* Создание
* Вывод списка
* Подключение через CLI
# Установка
Потребуются пакеты typer, prettytable, requests. Ставим их из файла:
```commandline
pip3 install --user -r requirements.txt
```
# Запуск
При первом запуске утилита спросит логин и пароль, после чего запишет их в ~/.config/twvdscli.ini в формате base64 через знак ":".
Утилиту можно поместить куда-нибудь в ~/.local/bin и поправить $PATH

9
requirements.txt Normal file
View File

@@ -0,0 +1,9 @@
certifi==2021.10.8
charset-normalizer==2.0.12
click==8.0.4
idna==3.3
prettytable==3.2.0
requests==2.27.1
typer==0.4.0
urllib3==1.26.9
wcwidth==0.2.5

775
twvdscli.py Executable file → Normal file
View File

@@ -2,21 +2,97 @@
import json import json
import sys import sys
import requests import requests
import typer import typer
import os import os
import configparser import configparser
import base64 import base64
from prettytable import PrettyTable
from typing import Optional
# For spinning wheel
from itertools import cycle
from time import sleep
app = typer.Typer() app = typer.Typer()
# twvdscli dbs
dbs_app = typer.Typer()
app.add_typer(dbs_app, name='dbs', help='Control Managed databases')
# twvdscli vds
servers_app = typer.Typer() servers_app = typer.Typer()
app.add_typer(servers_app, name='vds') app.add_typer(servers_app, name='vds', help='Control VDS Servers, their snapshots and backups')
# twvdscli vds backups
backups_app = typer.Typer()
servers_app.add_typer(backups_app, name='backup', help='Create/Delete backup')
# twvdscli vds snap
snapshot_app = typer.Typer()
servers_app.add_typer(snapshot_app, name='snap', help='Create/Rollback/Delete snapshot')
# twvdscli vds info
vds_info_app = typer.Typer()
servers_app.add_typer(vds_info_app, name='info', help='Get info about plans and os\'es')
class Dbaas:
"""
DataBases As A Service
"""
@staticmethod
def list():
url = 'https://public-api.timeweb.com/api/v1/dbs'
result = requests.get(
url=url,
headers=reqHeader
)
if not result.ok:
return None
return result.json()
@staticmethod
def get(db_id):
url = 'https://public-api.timeweb.com/api/v1/dbs/{db_id}'
result = requests.get(
url=url.format(db_id=db_id),
headers=reqHeader
)
if not result.ok:
return None
return result.json()
@staticmethod
def create(passwd, name, db_type):
url = 'https://public-api.timeweb.com/api/v1/dbs'
if db_type == 'postgres':
service_type = 357
else:
service_type = 341
data = dict(host="%", login="user", password=passwd, name=name, type=db_type,
hash_type="caching_sha2", service_type=service_type)
result = requests.post(
url=url,
json=data,
headers=reqHeader
)
if not result.ok:
return None
return result.json()
class Server: class Server:
"""
Not a server, but a backend.
Everything that works directly with API is here.
"""
@staticmethod @staticmethod
def get_list(): def get_list():
"""
Get list of VDSs'
"""
url = 'https://public-api.timeweb.com/api/v2/vds' url = 'https://public-api.timeweb.com/api/v2/vds'
result = requests.get( result = requests.get(
url=url, url=url,
@@ -26,8 +102,25 @@ class Server:
return None return None
return result.json() return result.json()
@staticmethod
def get_vds(vds_id):
"""
Get VDS info
"""
url = "https://public-api.timeweb.com/api/v2/vds/{vds_id}".format(vds_id=vds_id)
result = requests.get(
url=url,
headers=reqHeader
)
if not result.ok:
return None
return result.json()
@staticmethod @staticmethod
def start(vds_id): def start(vds_id):
"""
Start VDS
"""
uri = "https://public-api.timeweb.com/api/v1/vds/{id}/{action}" uri = "https://public-api.timeweb.com/api/v1/vds/{id}/{action}"
result = requests.post( result = requests.post(
uri.format(id=vds_id, action='start'), uri.format(id=vds_id, action='start'),
@@ -40,6 +133,9 @@ class Server:
@staticmethod @staticmethod
def stop(vds_id): def stop(vds_id):
"""
Stop VDS
"""
uri = "https://public-api.timeweb.com/api/v1/vds/{id}/{action}" uri = "https://public-api.timeweb.com/api/v1/vds/{id}/{action}"
result = requests.post( result = requests.post(
uri.format(id=vds_id, action='shutdown'), uri.format(id=vds_id, action='shutdown'),
@@ -50,43 +146,653 @@ class Server:
else: else:
return result.json() return result.json()
@staticmethod
def clone(vds_id):
"""
Clone VDS
"""
uri = "https://public-api.timeweb.com/api/v1/vds/{id}/{action}"
result = requests.post(
uri.format(id=vds_id, action='clone'),
headers=reqHeader
)
if not result.ok:
return None
else:
return result.json()
@staticmethod
def remove(vds_id):
"""
Remove VDS
"""
uri = "https://public-api.timeweb.com/api/v1/vds/{id}"
result = requests.delete(
uri.format(id=vds_id),
headers=reqHeader
)
if not result.ok:
return None
else:
return result.json()
class Backups:
@staticmethod
def create(vds_id):
disk_id = Server.get_vds(vds_id)['server']['disk_stats']['disk_id']
uri = "https://public-api.timeweb.com/api/v1/backups/vds/{id}/drive/{disk_id}"
result = requests.post(
uri.format(id=vds_id, disk_id=disk_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@staticmethod
def list(vds_id):
disk_id = Server.get_vds(vds_id)['server']['disk_stats']['disk_id']
uri = "https://public-api.timeweb.com/api/v1/backups/vds/{id}/drive/{disk_id}"
result = requests.get(
uri.format(id=vds_id, disk_id=disk_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@staticmethod
def remove(vds_id, backup_id):
disk_id = Server.get_vds(vds_id)['server']['disk_stats']['disk_id']
uri = "https://public-api.timeweb.com/api/v1/backups/{backup_id}/vds/{id}/drive/{disk_id}"
result = requests.delete(
uri.format(id=vds_id, disk_id=disk_id, backup_id=backup_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
class Snapshots:
@staticmethod
def get(vds_id):
uri = "https://public-api.timeweb.com/api/v1/restore-points/{vds_id}"
result = requests.get(
uri.format(vds_id=vds_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@staticmethod
def create(vds_id):
uri = "https://public-api.timeweb.com/api/v1/restore-points/{vds_id}/create"
result = requests.post(
uri.format(vds_id=vds_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@staticmethod
def remove(vds_id):
uri = "https://public-api.timeweb.com/api/v1/restore-points/{vds_id}/commit"
result = requests.post(
uri.format(vds_id=vds_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@staticmethod
def restore(vds_id):
uri = "https://public-api.timeweb.com/api/v1/restore-points/{vds_id}/rollback"
result = requests.post(
uri.format(vds_id=vds_id),
headers=reqHeader
)
if result.ok:
return result.json()
else:
return None
@snapshot_app.command("get")
def get_snap(vds_id: Optional[int] = typer.Argument(None)):
"""
Get snapshot
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Snapshots.get(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
x = PrettyTable()
x.field_names = ["VDS ID", "ID", "Created", "Expire"]
x.add_row([vds_id, result['restore_point']['id'], result['restore_point']['created_at'], result['restore_point']['expired_at']])
print(x)
@snapshot_app.command("create")
def create_snap(vds_id: Optional[int] = typer.Argument(None)):
"""
Create snapshot
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Snapshots.create(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
print(typer.style("Success", fg=typer.colors.GREEN))
@snapshot_app.command("restore")
def rollback_snap(vds_id: Optional[int] = typer.Argument(None)):
"""
Restore VDS from snapshot
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Snapshots.restore(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
print(typer.style("Success", fg=typer.colors.GREEN))
@snapshot_app.command("remove")
def remove_snap(vds_id: Optional[int] = typer.Argument(None)):
"""
Remove snapshot
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Snapshots.remove(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
print(typer.style("Success", fg=typer.colors.GREEN))
@backups_app.command("create")
def create_backup(vds_id: Optional[int] = typer.Argument(None)):
"""
Create backup of main disk
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Backups.create(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
print(typer.style("Success", fg=typer.colors.GREEN))
@backups_app.command("list")
def list_backup(vds_id: Optional[int] = typer.Argument(None)):
"""
List backups
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Backups.list(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
x = PrettyTable()
x.field_names = ["id", "Date", "Size", "Cost", "Mounted", "status"]
for i in result['backups']:
x.add_row([i['id'], i['c_date'], i['drive_size'], i['cost_backup'], i['mounted'], i['status']])
print(x)
@backups_app.command("remove")
def remove_backup(vds_id: Optional[int] = typer.Argument(None), backup_id: int = typer.Option(...)):
"""
Remove backup of main disk
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
result = Backups.remove(vds_id, backup_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
print(typer.style("Success", fg=typer.colors.GREEN))
@app.command("balance") @app.command("balance")
def get_balance(): def get_balance():
"""
Show balance and Monthly costs
"""
response = requests.get("https://public-api.timeweb.com/api/v1/accounts/finances", headers=reqHeader) response = requests.get("https://public-api.timeweb.com/api/v1/accounts/finances", headers=reqHeader)
if not response.ok: if not response.ok:
print(typer.style("Error", fg=typer.colors.RED)) print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1) sys.exit(1)
else: else:
x = PrettyTable()
x.field_names = ["Balance", "Monthly cost"]
response = response.json() response = response.json()
print('{0:6} {1:}'.format('balance', 'monthly_cost')) x.add_row([response['finances']['balance'], response['finances']['monthly_cost']])
print('{0:6} {1:}'.format(response['finances']['balance'], response['finances']['monthly_cost'])) print(x)
@dbs_app.command("create")
def dbs_create(passwd: str = typer.Option(..., help="DB password"),
name: str = typer.Option(..., help="DB Name"),
db_type: str = typer.Option(..., help="mysql5/mysql/postgres"),
raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Create database
"""
# service_type == 341 - mysql
# service_type == 357 - pgsql
result = Dbaas.create(passwd=passwd, name=name, db_type=db_type)
if raw:
print(result)
return
# We need to get id from result['db']['id']
db_id = result['db']['id']
for frame in cycle(r'-\|/'):
state = Dbaas.get(db_id)
if state:
if state['db']['status'] == 'started':
print(typer.style("\nCreated DB: " + name, fg=typer.colors.GREEN))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@dbs_app.command("list")
def dbs_list(raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Show list of DBs:
ID, State, Name, IP, local IP, Password, Type
"""
list_of_dbaas = Dbaas.list()
if raw:
print(list_of_dbaas)
return
x = PrettyTable()
x.field_names = ['id', 'state', 'name', 'ip', 'local_ip', 'password', 'type']
if list_of_dbaas is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
for i in list_of_dbaas['dbs']:
if i['status'] == 'started':
state = typer.style("Running", fg=typer.colors.GREEN)
# elif i['status'] == 'off':
# state = typer.style('Stopped', fg=typer.colors.RED)
else:
state = i['status']
if i['type'] == 'mysql':
type_of_db = 'MySQL 8'
elif i['type'] == 'mysql5':
type_of_db = 'MySQL 5.7'
elif i['type'] == 'postgres':
type_of_db = 'PostgreSQL 13'
else:
type_of_db = i['type']
x.add_row([
i['id'],
state,
i['name'],
i['ip'],
i['local_ip'],
i['password'],
type_of_db
])
print(x)
@dbs_app.command("goto")
def dbs_connect(db_id: Optional[int] = typer.Argument(None)):
"""
Connect to DB CLI
"""
cmd_mysql = "mysql -u {login} -p{password} -h {ip} -P 3306 -D default_db"
cmd_psql = "psql -d default_db -U {login} -W -p 5432 -h {ip}"
# Get DB ID if not specified
if db_id is None:
dbs_list()
db_id = input("Enter DB ID: ")
# Get type of DB, password, IP
db_data = Dbaas.get(db_id)
db_type = db_data['db']['type']
db_pass = db_data['db']['password']
db_ip = db_data['db']['ip']
db_user = db_data['db']['login']
if db_type == 'mysql' or db_type == 'mysql5':
os.system(cmd_mysql.format(ip=db_ip, password=db_pass, login=db_user))
elif db_type == 'postgres':
print("Password: ", db_pass)
os.system(cmd_psql.format(ip=db_ip, login=db_user))
@vds_info_app.command("plans")
def vds_plans(raw: bool = typer.Option(False, help="Get result as raw json"),
sort_by: str = typer.Option(None, help="sort results by value/cpu/ram/disk")):
uri = "https://public-api.timeweb.com/api/v1/presets"
result = requests.get(uri, headers=reqHeader)
if not result.ok:
print('Error')
sys.exit(1)
# If raw - print raw result and exit
if raw:
print(result.text)
sys.exit(0)
# else print pretty
x = PrettyTable()
x.field_names = ['id', 'cpus', 'ram', 'disk', 'value', 'name', 'description']
result = result.json()
print("Total: "+ str(result['meta']['total']))
# result = result
for i in result['presets']:
x.add_row([
i['id'],
i['cpu'],
i['ram'],
i['drive'],
i['discount_value'],
i['name'],
i['description']
])
if sort_by in ('cpus', 'ram', 'disk', 'value'):
x.sortby = sort_by
elif not sort_by is None:
print("No such sort")
print(x)
@vds_info_app.command("os")
def vds_oses(raw: bool = typer.Option(False, help="Get result as raw json")):
uri = "https://public-api.timeweb.com/api/v1/os"
result = requests.get(uri, headers=reqHeader)
if not result.ok:
print('Error')
sys.exit(1)
# If raw - print raw result and exit
if raw:
print(result.text)
sys.exit(0)
# else print pretty
x = PrettyTable()
x.field_names = ['id', 'fullname', 'family', 'name', 'latin', 'available']
result = result.json()
print("Total: "+ str(result['meta']['total']))
# result = result
for i in result['os']:
x.add_row([
i['id'],
i['os_caption'],
i['os_type'],
i['os_name'],
i['os_latin'],
i['is_public']
])
print(x)
@servers_app.command("create")
def vds_create(
name: str = typer.Option(..., help="VDS Name"),
os_id: int = typer.Option(..., help="OS ID"),
preset: int = typer.Option(17, help="Preset ID"),
comment: str = typer.Option("", help="Comment")
):
# get user group
group_uri = "https://public-api.timeweb.com/api/v1/accounts/{user}/group"
# get username from saved base64
config = configparser.ConfigParser()
config.read(os.path.join(os.getenv('HOME'), '.config', 'twvdscli.ini'))
based = config.get('api', 'key', fallback=None)
based = base64.b64decode(based)
based = str(based, 'utf-8')
user = based.split(':')[0]
group_id = requests.get(
group_uri.format(user=user), headers=reqHeader
)
# finally get group id
if group_id.ok:
group_id = group_id.json()['groups'][0]['id']
data = {
"server": {
"configuration": {
"caption": name,
# "disk_size": 5, # dont give a fuck
# "network_bandwidth": 100, # dont give a fuck
"os": os_id, # 47 - ubuntu 18.04
# "xen_cpu": 2, # dont give a fuck
# "xen_ram": 4096, # dont give a fuck
"ddos_guard": False
},
"comment": comment,
"group_id": group_id, # https://public-api.timeweb.com/api/v1/accounts/{user}/group
"name": "string", # what is this for?
"preset_id": preset, # you can not create vds without this, but how to create flexible vds? (preset example: 20)
"install_ssh_key": "",
"server_id": None,
"local_networks": []
}
}
response = requests.post(
"https://public-api.timeweb.com/api/v1/vds",
headers=reqHeader,
json=data
)
if not response.ok:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
response = response.json()
for frame in cycle(r'-\|/'):
state = Server.get_vds(response['server']['id'])
if state:
if state['server']['status'] == 'on':
print(typer.style("\nCreated: " + response['server']['configuration']['caption'], fg=typer.colors.GREEN))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@servers_app.command("goto")
def vds_goto(vds_id: Optional[int] = typer.Argument(None),
port: int = typer.Option(22, help="Specify non standart SSH port.")):
"""
Connect via SSH to VDS
"""
if vds_id is None:
vds_list()
vds_id = input("Enter VDS ID: ")
vds = Server.get_vds(vds_id)
ip = vds['server']['ip']
os.system('ssh -p {port} root@{ip}'.format(port=port, ip=ip))
@servers_app.command("start") @servers_app.command("start")
def vds_start(vds_id: int = typer.Argument(...)): def vds_start(vds_id: Optional[int] = typer.Argument(None), raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Start VDS
"""
if vds_id is None:
if raw:
print(
dict(
error="No VDS ID provided"
)
)
return 1
vds_list()
vds_id = input("Enter VDS ID: ")
result = Server.start(vds_id) result = Server.start(vds_id)
if result is None: if result is None:
print(typer.style("Error", fg=typer.colors.RED)) print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1) sys.exit(1)
if raw:
print(result) print(result)
return
for frame in cycle(r'-\|/'):
state = Server.get_vds(vds_id)
if state:
if state['server']['status'] == 'on':
print(typer.style("\nRunning", fg=typer.colors.GREEN))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@servers_app.command("stop") @servers_app.command("stop")
def vds_stop(vds_id: int = typer.Argument(...)): def vds_stop(vds_id: Optional[int] = typer.Argument(None), raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Stop VDS
"""
if vds_id is None:
if raw:
print(
dict(
error="No VDS ID provided"
)
)
return 1
vds_list()
vds_id = input("Enter VDS ID: ")
result = Server.stop(vds_id) result = Server.stop(vds_id)
if result is None: if result is None:
print(typer.style("Error", fg=typer.colors.RED)) print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1) sys.exit(1)
if raw:
print(result) print(result)
for frame in cycle(r'-\|/'):
state = Server.get_vds(vds_id)
if state:
if state['server']['status'] == 'off':
print(typer.style("\nStopped", fg=typer.colors.RED))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@servers_app.command("clone")
def vds_clone(vds_id: Optional[int] = typer.Argument(None),
raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Clone VDS
"""
if vds_id is None:
if raw:
print(
dict(
error="No VDS ID provided"
)
)
sys.exit(1)
vds_list()
vds_id = input("Enter VDS ID: ")
new_vds = Server.clone(vds_id)
if new_vds is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
if raw:
print(new_vds)
return
new_vds = new_vds['server']
for frame in cycle(r'-\|/'):
state = Server.get_vds(new_vds['id'])
if state:
if state['server']['status'] == 'on':
print(typer.style("\nCloned: " + new_vds['configuration']['caption'], fg=typer.colors.GREEN))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@servers_app.command("remove")
def vds_remove(vds_id: Optional[int] = typer.Argument(None),
raw: bool = typer.Option(False, help="Get result as raw json")):
"""
Remove VDS
"""
if vds_id is None:
if raw:
print(
dict(
error="No VDS ID provided"
)
)
sys.exit(1)
vds_list()
vds_id = input("Enter VDS ID: ")
result = Server.remove(vds_id)
if result is None:
print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1)
else:
if raw:
print(result)
sys.exit(0)
for frame in cycle(r'-\|/'):
state = Server.get_vds(vds_id)
if not state.get('server'):
print(typer.style("\nDeleted", fg=typer.colors.RED))
break
print('\r', frame, sep='', end='', flush=True)
sleep(0.1)
@servers_app.command("list") @servers_app.command("list")
def vds_list(): def vds_list():
"""
Show list of VDSes
ID, State, Name, IP, CPUs, Ram, Disk
"""
list_of_servers = Server.get_list() list_of_servers = Server.get_list()
# print('{0:7} {1:17} {5:16} {2:2} {3:5} {4:4}'.format('state', 'name', 'vcpus', 'memory', 'disk', 'ip')) x = PrettyTable()
x.field_names = ['id', 'state', 'name', 'ip', 'cpus', 'ram', 'disk']
if list_of_servers is None: if list_of_servers is None:
print(typer.style("Error", fg=typer.colors.RED)) print(typer.style("Error", fg=typer.colors.RED))
sys.exit(1) sys.exit(1)
@@ -97,23 +803,26 @@ def vds_list():
state = typer.style('Stopped', fg=typer.colors.RED) state = typer.style('Stopped', fg=typer.colors.RED)
else: else:
state = i['status'] state = i['status']
print('{0:6} {1:15} {2:20} {3:16} {4:2} {5:5} {6:4} '.format(i['id'], state, i['name'], i['ip'], i['configuration']['cpu'], i['configuration']['ram'], i['configuration']['disk_size'])) x.add_row([
i['id'],
state,
i['name'],
i['ip'],
i['configuration']['cpu'],
i['configuration']['ram'],
i['configuration']['disk_size']
])
print(x)
def auth(login, passwd): def auth(based):
""" """
Convert login:pass into base64 and make a request Get access token based on base64'ed login:password
:param login:
:param passwd:
:return:
""" """
based = base64.b64encode(str(login+":"+passwd).encode('utf-8')) headers = {"Authorization": "Basic " + based}
headers = {"Authorization": "Basic " + based.decode('utf-8')}
result = requests.post( result = requests.post(
'https://public-api.timeweb.com/api/v2/auth', 'https://public-api.timeweb.com/api/v2/auth',
json=dict(refresh_token="string"),
headers=headers headers=headers
) )
if not result.ok: if not result.ok:
@@ -122,29 +831,41 @@ def auth(login, passwd):
result = result.content.decode('utf-8') result = result.content.decode('utf-8')
result = json.loads(result) result = json.loads(result)
result = result['access_token'] result = result['access_token']
print(result)
return result return result
def get_api_key(): def get_api_key():
"""
Load base64'ed login:pass and get access token
"""
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(os.path.join(os.getenv('HOME'), '.config', 'twvdscli.ini')) config.read(os.path.join(os.getenv('HOME'), '.config', 'twvdscli.ini'))
result = config.get('api', 'key', fallback=None) based = config.get('api', 'key', fallback=None)
if result is None: if based is None:
login = input("Enter login ") login = input("Enter login ")
passwd = input("Enter passwd ") passwd = input("Enter passwd ")
result = auth(login, passwd) based = base64.b64encode(str(login + ":" + passwd).encode('utf-8'))
based = based.decode('utf-8')
config.add_section('api') config.add_section('api')
config.set('api', 'key', result) config.set('api', 'key', based)
with open(os.path.join(os.getenv('HOME'), '.config', 'twvdscli.ini'), 'w') as configfile: with open(os.path.join(os.getenv('HOME'), '.config', 'twvdscli.ini'), 'w') as configfile:
config.write(configfile) config.write(configfile)
result = auth(based)
return result return result
if __name__ == '__main__': def main():
apikey = get_api_key() apikey = get_api_key()
reqHeader = {"Authorization": "Bearer " + apikey} if apikey is None:
print(typer.style("Auth Error", fg=typer.colors.RED))
sys.exit(1)
reqHeader['Authorization'] += apikey
app() app()
if __name__ == '__main__':
reqHeader = {"Authorization": "Bearer "}
main()