This commit is contained in:
parent
44ee9c7cad
commit
0c884e01cf
14 changed files with 302 additions and 58 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Environment
|
||||
.vscode/
|
||||
*venv/
|
||||
.old/
|
||||
|
||||
# Python stuff
|
||||
__pycache__/
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
import importlib.metadata
|
||||
__version__ = importlib.metadata.version(__package__ or "dockge_cli")
|
||||
|
||||
from .dockge_cli import cli
|
||||
|
|
110
dockge_cli/components/communicate.py
Normal file
110
dockge_cli/components/communicate.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
import time
|
||||
from typing import Any
|
||||
import socketio
|
||||
import socketio.exceptions
|
||||
from . import storage
|
||||
|
||||
class DockgeConnection:
|
||||
class LoginException(BaseException):
|
||||
pass
|
||||
|
||||
_sio: socketio.Client
|
||||
_host: str
|
||||
_logged_in: bool
|
||||
|
||||
_stacklist: Any
|
||||
|
||||
def __init__(self):
|
||||
self._logged_in = False
|
||||
self._host = storage.get("host")
|
||||
self._sio = socketio.Client(logger=False, engineio_logger=False)
|
||||
self._stacklist = None
|
||||
self._init_events()
|
||||
|
||||
def _init_events(self):
|
||||
@self._sio.event
|
||||
def connect():
|
||||
self.connect()
|
||||
print("Connected!")
|
||||
|
||||
@self._sio.event
|
||||
def disconnect():
|
||||
self._logged_in = False
|
||||
print("Disconnected!")
|
||||
|
||||
@self._sio.event
|
||||
def connect_error(data):
|
||||
print("The connection failed!")
|
||||
print(data)
|
||||
|
||||
@self._sio.on('info')
|
||||
def info(data):
|
||||
if all(k in data for k in ("version", "latestVersion", "isContainer", "primaryHostname")):
|
||||
print(f"Dockge Version: {data['version']}")
|
||||
|
||||
@self._sio.on('agent')
|
||||
def agent(*args):
|
||||
if args[0] == "stackList":
|
||||
self._stacklist = args[1]
|
||||
|
||||
# Callbacks
|
||||
def _login_process(self, data):
|
||||
success = False
|
||||
if "ok" in data and "token" in data:
|
||||
print(f"Login was {'successful' if data['ok'] else 'unsuccessful'}!")
|
||||
success = True
|
||||
else:
|
||||
print("Issue with login procedure")
|
||||
return success
|
||||
|
||||
# Functions
|
||||
def connect_and_login(self):
|
||||
self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
if self._logged_in:
|
||||
return
|
||||
|
||||
data = None
|
||||
retry, count = True, 0
|
||||
|
||||
while retry and count < 5:
|
||||
try:
|
||||
data = self._sio.call(
|
||||
"login",
|
||||
{
|
||||
"username": storage.get("username", encoded=True),
|
||||
"password": storage.get("password", encoded=True),
|
||||
"token":""
|
||||
},
|
||||
timeout=5
|
||||
)
|
||||
retry = False
|
||||
except socketio.exceptions.TimeoutError:
|
||||
retry = True
|
||||
count += 1
|
||||
|
||||
if retry or not self._login_process(data):
|
||||
raise self.LoginException
|
||||
self._logged_in = True
|
||||
|
||||
def list_stacks(self):
|
||||
self._sio.emit("agent", ("", "requestStackList"))
|
||||
while self._stacklist is None:
|
||||
time.sleep(0.5)
|
||||
retval = self._stacklist
|
||||
self._stacklist = None
|
||||
return retval
|
||||
|
||||
def list_stack(self, name: str):
|
||||
ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=5)
|
||||
return ret
|
||||
|
||||
def restart(self, name):
|
||||
ret = self._sio.call("agent", ("", "restartStack", name), timeout=10)
|
||||
return ret
|
||||
|
||||
def disconnect(self):
|
||||
self._sio.emit("logout")
|
||||
self._sio.disconnect()
|
|
@ -1,31 +0,0 @@
|
|||
from getpass import getpass
|
||||
from urllib.parse import urlparse
|
||||
from . import storage
|
||||
|
||||
|
||||
def exec_command(command, extra_args):
|
||||
match command:
|
||||
case "login":
|
||||
storage.set("username", input("Username: "), encoded=True)
|
||||
storage.set("password", getpass("Password: "), encoded=True)
|
||||
return
|
||||
case "logout":
|
||||
storage.remove("username")
|
||||
storage.remove("password")
|
||||
return
|
||||
case "host":
|
||||
if len(extra_args) > 0:
|
||||
res = urlparse(extra_args[0])
|
||||
if all([res.scheme, res.netloc]):
|
||||
storage.set("host", extra_args[0])
|
||||
else:
|
||||
raise Exception(f"Malformed URL {extra_args[0]}")
|
||||
return
|
||||
print(storage.get("host"))
|
||||
case "exit":
|
||||
storage.clear()
|
||||
return
|
||||
case _:
|
||||
print("Not implemented")
|
||||
return
|
||||
|
|
@ -2,27 +2,30 @@ import argparse
|
|||
import sys
|
||||
|
||||
from .. import __version__
|
||||
from ..models import commands
|
||||
|
||||
commands = [
|
||||
"host",
|
||||
"login",
|
||||
"logout",
|
||||
"list",
|
||||
"restart",
|
||||
"update",
|
||||
"exit"
|
||||
]
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Arguments(argparse.Namespace):
|
||||
command: str
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Credentials(argparse.Namespace):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
credentialparser = argparse.ArgumentParser(
|
||||
prog="login",
|
||||
description="Subparser for login credentials provided by CI"
|
||||
)
|
||||
credentialparser.add_argument("--username", action="store", type=str, required=True)
|
||||
credentialparser.add_argument("--password", action="store", type=str, required=True)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="dockge_cli",
|
||||
description="CLI interface for interacting with Dockge",)
|
||||
|
||||
parser.add_argument("command", choices=commands, action="store", type=str, default=None)
|
||||
parser.add_argument("command", choices=list(commands.keys()), action="store", type=str, default=None)
|
||||
parser.add_argument("--version", action="version", version=f"dockge_cli {__version__}")
|
||||
|
||||
args = Arguments()
|
||||
|
||||
args, extra_args = parser.parse_known_args(sys.argv[1:], namespace=args)
|
||||
|
|
61
dockge_cli/components/run.py
Normal file
61
dockge_cli/components/run.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from getpass import getpass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from . import storage
|
||||
from .utils import display_help, stack_formatter, status_formatter, restart_formatter
|
||||
from .parser import Credentials, credentialparser
|
||||
from .communicate import DockgeConnection
|
||||
|
||||
def exec_command(command, extra_args):
|
||||
match command:
|
||||
case "help":
|
||||
display_help(extra_args)
|
||||
case "login":
|
||||
if len(extra_args) > 0:
|
||||
credentials = credentialparser.parse_args(extra_args, namespace=Credentials)
|
||||
storage.put("username", credentials.username, encoded=True)
|
||||
storage.put("password", credentials.password, encoded=True)
|
||||
return
|
||||
storage.put("username", input("Username: "), encoded=True)
|
||||
storage.put("password", getpass("Password: "), encoded=True)
|
||||
case "logout":
|
||||
storage.remove("username")
|
||||
storage.remove("password")
|
||||
case "host":
|
||||
if len(extra_args) > 0:
|
||||
res = urlparse(extra_args[0])
|
||||
if all([res.scheme, res.netloc]):
|
||||
host = extra_args[0].rstrip("/").replace("wss://", "").replace("https://", "")
|
||||
storage.put("host", host)
|
||||
else:
|
||||
raise ValueError(f"Malformed URL {extra_args[0]}")
|
||||
print(storage.get("host"))
|
||||
case "exit":
|
||||
storage.clear()
|
||||
case "list":
|
||||
con = DockgeConnection()
|
||||
con.connect_and_login()
|
||||
stack_formatter(con.list_stacks())
|
||||
con.disconnect()
|
||||
case "status":
|
||||
if extra_args is None:
|
||||
raise ValueError
|
||||
con = DockgeConnection()
|
||||
con.connect_and_login()
|
||||
status_formatter(con.list_stack(extra_args[0]))
|
||||
con.disconnect()
|
||||
case "restart":
|
||||
if extra_args is None:
|
||||
raise ValueError
|
||||
con = DockgeConnection()
|
||||
con.connect_and_login()
|
||||
restart_formatter(con.restart(extra_args[0]))
|
||||
con.disconnect()
|
||||
case "debug":
|
||||
con = DockgeConnection()
|
||||
con.connect_and_login()
|
||||
stack_formatter(con.list_stacks())
|
||||
print("fgo-ta-com", con.list_stack("fgo-ta-com"))
|
||||
con.disconnect()
|
||||
case _:
|
||||
print("Not implemented")
|
|
@ -3,37 +3,37 @@ import pathlib
|
|||
import base64
|
||||
import yaml
|
||||
|
||||
_storagepath = pathlib.Path(__file__).parents[1] / ".temp"
|
||||
_storagepath = pathlib.Path(__file__).parents[1] / ".temp"
|
||||
_file = _storagepath / "storage.yaml"
|
||||
|
||||
_storagepath.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def fileexists():
|
||||
if not _file.exists():
|
||||
with open(_file, 'a'):
|
||||
with open(_file, 'a', encoding="utf-8"):
|
||||
os.utime(_file, None)
|
||||
|
||||
def set(key: str, value: str, encoded=False):
|
||||
def put(key: str, value: str, encoded=False):
|
||||
fileexists()
|
||||
with open(_file, "r+") as file:
|
||||
with open(_file, "r+", encoding="utf-8") as file:
|
||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
||||
content.update({ key: base64.b64encode(value.encode()) if encoded else value })
|
||||
with open(_file, "w+") as file:
|
||||
with open(_file, "w+", encoding="utf-8") as file:
|
||||
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
||||
|
||||
def remove(key: str):
|
||||
fileexists()
|
||||
with open(_file, "r") as file:
|
||||
with open(_file, "r", encoding="utf-8") as file:
|
||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
||||
content.pop(key, None)
|
||||
with open(_file, "w+") as file:
|
||||
with open(_file, "w+", encoding="utf-8") as file:
|
||||
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
||||
|
||||
def get(key: str, encoded=False):
|
||||
value: str = None
|
||||
if not _file.exists():
|
||||
return None
|
||||
with open(_file, "r") as file:
|
||||
with open(_file, "r", encoding="utf-8") as file:
|
||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
|
||||
value = content.get(key, None)
|
||||
if value is None:
|
||||
|
|
30
dockge_cli/components/utils.py
Normal file
30
dockge_cli/components/utils.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from tabulate import tabulate
|
||||
from ..models import commands, StackStatus
|
||||
|
||||
def display_help(extra_args):
|
||||
if not extra_args:
|
||||
print(f"{commands['help'].description}")
|
||||
return
|
||||
if len(extra_args) > 1:
|
||||
raise ValueError("Invalid arguments for help command")
|
||||
print(f"{commands[extra_args[0]].description}")
|
||||
|
||||
def stack_formatter(stacks):
|
||||
if not stacks["ok"]:
|
||||
raise RuntimeError("Stack GET didn't work")
|
||||
|
||||
table, headers = [], ["Stackname", "Status"]
|
||||
for key, val in stacks["stackList"].items():
|
||||
table.append([key, StackStatus(val['status']).name])
|
||||
|
||||
print(tabulate(table, headers=headers, tablefmt="github"), "\n")
|
||||
|
||||
def status_formatter(status):
|
||||
print(f"Is Stack Ok? {'Yes' if status['ok'] else 'No'}")
|
||||
headers = ["Container", "Status"]
|
||||
table = [[k, v] for k, v in status["serviceStatusList"].items()]
|
||||
print(tabulate(table, headers=headers, tablefmt="github"), "\n")
|
||||
|
||||
def restart_formatter(status):
|
||||
print(f"Is Ok? {'Yes' if status['ok'] else 'No'}")
|
||||
print(f"Stack status: {status['msg']}")
|
|
@ -1,5 +1,5 @@
|
|||
from .components.parser import args, extra_args
|
||||
from .components.exec import exec_command
|
||||
from .components.run import exec_command
|
||||
|
||||
def cli():
|
||||
exec_command(args.command, extra_args)
|
||||
exec_command(args.command, extra_args)
|
||||
|
|
2
dockge_cli/models/__init__.py
Normal file
2
dockge_cli/models/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .commands import commands
|
||||
from .codes import StackStatus
|
7
dockge_cli/models/codes.py
Normal file
7
dockge_cli/models/codes.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from enum import Enum
|
||||
|
||||
class StackStatus(Enum):
|
||||
# pylint: disable=invalid-name
|
||||
inactive = 1
|
||||
running = 3
|
||||
exited = 4
|
58
dockge_cli/models/commands.py
Normal file
58
dockge_cli/models/commands.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
class CommandListing(BaseModel):
|
||||
command: str
|
||||
description: str
|
||||
|
||||
cmd_host = CommandListing(
|
||||
command="host",
|
||||
description="Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI"
|
||||
)
|
||||
|
||||
cmd_login = CommandListing(
|
||||
command="login",
|
||||
description="Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
|
||||
)
|
||||
|
||||
cmd_logout = CommandListing(
|
||||
command="logout",
|
||||
description="Removes the credentials from the local storage.",
|
||||
)
|
||||
|
||||
cmd_list = CommandListing(
|
||||
command="list",
|
||||
description="Lists all available stacks with their status",
|
||||
)
|
||||
|
||||
cmd_status = CommandListing(
|
||||
command="status",
|
||||
description="Returns the status of one stack",
|
||||
)
|
||||
|
||||
cmd_restart = CommandListing(
|
||||
command="restart",
|
||||
description="Restarts a given stack",
|
||||
)
|
||||
|
||||
cmd_update = CommandListing(
|
||||
command="update",
|
||||
description="Updates a stack",
|
||||
)
|
||||
|
||||
cmd_exit = CommandListing(
|
||||
command="exit",
|
||||
description="Exits the CLI - this will reset all settings, including credentials and host",
|
||||
)
|
||||
|
||||
cmd_debug = CommandListing(
|
||||
command="debug",
|
||||
description="debug",
|
||||
)
|
||||
|
||||
cmd_help = CommandListing(
|
||||
command="help",
|
||||
description="Displays helping hints for commands",
|
||||
)
|
||||
|
||||
commandlist = [cmd_host, cmd_login, cmd_logout, cmd_list, cmd_restart, cmd_update, cmd_exit, cmd_debug, cmd_help, cmd_status]
|
||||
commands = { k.command: k for k in commandlist }
|
|
@ -2,7 +2,11 @@
|
|||
name = "dockge_cli"
|
||||
version = "0.0.1-a.1"
|
||||
dependencies = [
|
||||
"pyyaml~=6.0.1"
|
||||
"pyyaml~=6.0.1",
|
||||
"pydantic~=2.8.0",
|
||||
"python-socketio~=5.11.3",
|
||||
"websocket-client~=1.8.0",
|
||||
"tabulate ~=0.9.0",
|
||||
]
|
||||
requires-python = ">= 3.10"
|
||||
authors = [{name = "Firq", email = "firelp42@gmail.com"}]
|
||||
|
@ -17,14 +21,15 @@ classifiers = [
|
|||
]
|
||||
|
||||
[project.scripts]
|
||||
dockge-cli = "dockge_cli:cli"
|
||||
dockge-cli = "dockge_cli.dockge_cli:cli"
|
||||
dockge = "dockge_cli.dockge_cli:cli"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["dockge_cli*"]
|
||||
|
||||
[tool.pylint."MAIN"]
|
||||
disable = [ "line-too-long", "missing-module-docstring" ]
|
||||
disable = [ "line-too-long", "missing-module-docstring", "missing-function-docstring", "missing-class-docstring" ]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools >= 61.0"]
|
||||
|
|
Loading…
Reference in a new issue