tools: create a new tooling repo
This commit is contained in:
parent
c8a4033220
commit
63292cc337
72
services/tools/cli/.gitignore
vendored
Normal file
72
services/tools/cli/.gitignore
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Byte-compiled files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Jupyter Notebook checkpoints
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Python virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Test and coverage reports
|
||||||
|
.coverage
|
||||||
|
*.cover
|
||||||
|
*.log
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.whl
|
||||||
|
|
||||||
|
# Pytest cache
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# MyPy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# Pylint
|
||||||
|
pylint-report.txt
|
||||||
|
pylint-output.txt
|
||||||
|
|
||||||
|
# Other files
|
||||||
|
*.DS_Store
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.mo
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
18
services/tools/cli/args_utils.py
Normal file
18
services/tools/cli/args_utils.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from argparse import ArgumentTypeError
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_full_path(relative_path):
|
||||||
|
if not os.path.isabs(relative_path):
|
||||||
|
current_dir = os.getcwd()
|
||||||
|
full_path = os.path.join(current_dir, relative_path)
|
||||||
|
else:
|
||||||
|
full_path = relative_path
|
||||||
|
|
||||||
|
return full_path
|
||||||
|
|
||||||
|
def dir_arg_type(path):
|
||||||
|
full_path = get_full_path(path)
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
return full_path
|
||||||
|
else:
|
||||||
|
raise ArgumentTypeError(f"{path} nor {full_path} are not a valid path")
|
||||||
21
services/tools/cli/base.py
Normal file
21
services/tools/cli/base.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class BaseCLi:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
self.logger.addHandler(handler)
|
||||||
|
self.logger.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
def info(self, message):
|
||||||
|
self.logger.info(message)
|
||||||
|
|
||||||
|
def warning(self, message):
|
||||||
|
self.logger.warning(message)
|
||||||
|
|
||||||
|
def error(self, message, exit_code=1):
|
||||||
|
self.logger.error(message)
|
||||||
|
sys.exit(exit_code)
|
||||||
46
services/tools/cli/constants.py
Normal file
46
services/tools/cli/constants.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
PORTAINER_HOST_URLS = {
|
||||||
|
'vm-tools-100-55': 'https://portainer55.davydovcloud.com',
|
||||||
|
'vm-mixed-100-98': 'https://portainer98.davydovcloud.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
PORTAINER_STACK_CLI_ACTIONS = {
|
||||||
|
'LIST': {
|
||||||
|
'value': 'list',
|
||||||
|
'arg_required': False,
|
||||||
|
'arg_name': '',
|
||||||
|
},
|
||||||
|
'CREATE': {
|
||||||
|
'value': 'create',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-dir-path',
|
||||||
|
},
|
||||||
|
'REMOVE': {
|
||||||
|
'value': 'remove',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-id',
|
||||||
|
},
|
||||||
|
'REDEPLOY': {
|
||||||
|
'value': 'redeploy',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-id',
|
||||||
|
},
|
||||||
|
'START': {
|
||||||
|
'value': 'start',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-id',
|
||||||
|
},
|
||||||
|
'STOP': {
|
||||||
|
'value': 'stop',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-id',
|
||||||
|
},
|
||||||
|
'INFO': {
|
||||||
|
'value': 'info',
|
||||||
|
'arg_required': True,
|
||||||
|
'arg_name': 'stack-id',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_PORTAINER_JWT_CONFIG_FILE = '.portainer_cli_config'
|
||||||
|
FUNCTIONAL_USER_USERNAME_ENV = 'PORTAINER_USERNAME'
|
||||||
|
FUNCTIONAL_USER_PASSWORD_ENV = 'PORTAINER_PASSWORD'
|
||||||
76
services/tools/cli/portainer_host.py
Normal file
76
services/tools/cli/portainer_host.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import json
|
||||||
|
from base import BaseCLi
|
||||||
|
from constants import (
|
||||||
|
DEFAULT_PORTAINER_JWT_CONFIG_FILE,
|
||||||
|
FUNCTIONAL_USER_USERNAME_ENV,
|
||||||
|
FUNCTIONAL_USER_PASSWORD_ENV,
|
||||||
|
)
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class MissingEnvironmentVariable(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmptyConfigException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PortainerHost(BaseCLi):
|
||||||
|
|
||||||
|
def __init__(self, host_url):
|
||||||
|
super().__init__()
|
||||||
|
self.host_url = host_url
|
||||||
|
self.api_url = f'{host_url}/api'
|
||||||
|
self.jwt_config = []
|
||||||
|
home_directory = os.path.expanduser("~")
|
||||||
|
config_file_path = os.path.join(home_directory, DEFAULT_PORTAINER_JWT_CONFIG_FILE)
|
||||||
|
try:
|
||||||
|
self.read_current_config(config_file_path)
|
||||||
|
except EmptyConfigException:
|
||||||
|
self.create_config(config_file_path)
|
||||||
|
|
||||||
|
def read_current_config(self, file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as file:
|
||||||
|
self.jwt_config = json.load(file)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Configuration does not exist. Creating...")
|
||||||
|
raise EmptyConfigException()
|
||||||
|
|
||||||
|
hostnames = [config['hostname'] for config in self.jwt_config]
|
||||||
|
if self.host_url not in hostnames:
|
||||||
|
raise EmptyConfigException()
|
||||||
|
|
||||||
|
def create_config(self, file_path):
|
||||||
|
config = {
|
||||||
|
"hostname": self.host_url,
|
||||||
|
"jwt": self.get_jwt_token(),
|
||||||
|
}
|
||||||
|
self.jwt_config.append(config)
|
||||||
|
with open(file_path, 'w') as file:
|
||||||
|
json.dump(self.jwt_config, file, indent=4)
|
||||||
|
|
||||||
|
def get_jwt_token(self):
|
||||||
|
def get_env(env_key):
|
||||||
|
try:
|
||||||
|
return os.environ[env_key]
|
||||||
|
except KeyError:
|
||||||
|
raise MissingEnvironmentVariable(f"{env_key} env does not exist")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Username": get_env(FUNCTIONAL_USER_USERNAME_ENV),
|
||||||
|
"Password": get_env(FUNCTIONAL_USER_PASSWORD_ENV)
|
||||||
|
}
|
||||||
|
auth_endpoint = f'{self.api_url}/auth'
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
auth_endpoint,
|
||||||
|
json=payload
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
self.error(f'Return code {response.status_code} from {auth_endpoint} with message {response.text}')
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.error("An error occurred:", e)
|
||||||
|
data = response.json()
|
||||||
|
return data['jwt']
|
||||||
|
|
||||||
79
services/tools/cli/portainer_stack.py
Executable file
79
services/tools/cli/portainer_stack.py
Executable file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from args_utils import dir_arg_type
|
||||||
|
from base import BaseCLi
|
||||||
|
from constants import PORTAINER_HOST_URLS, PORTAINER_STACK_CLI_ACTIONS
|
||||||
|
from portainer_host import PortainerHost
|
||||||
|
|
||||||
|
CLI_DESCRIPTION='''
|
||||||
|
This cli is meant to be used to execute whoel LCM flow of
|
||||||
|
Portainer host stacks. Portainer host can be supplied separately,
|
||||||
|
or by the tag.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class PortainerStackCLi(BaseCLi):
|
||||||
|
|
||||||
|
def configure_args(self, parser: ArgumentParser):
|
||||||
|
parser.add_argument(
|
||||||
|
'-a',
|
||||||
|
'--action',
|
||||||
|
help='Action to be performed on Portainer host',
|
||||||
|
required=True,
|
||||||
|
choices=[action['value'] for action in PORTAINER_STACK_CLI_ACTIONS.values()]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--stack-dir-path',
|
||||||
|
help='Directory with the stack related files',
|
||||||
|
type=dir_arg_type
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-i',
|
||||||
|
'--stack-id',
|
||||||
|
help='Id of the stack in Portainer (see list)',
|
||||||
|
type=int
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--hostname',
|
||||||
|
help='Portainer hostname to be used',
|
||||||
|
required=True,
|
||||||
|
type=str,
|
||||||
|
choices=PORTAINER_HOST_URLS.keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_args(self, parser: ArgumentParser):
|
||||||
|
args = parser.parse_args()
|
||||||
|
required_args = {
|
||||||
|
'stack-dir-path': args.stack_dir_path,
|
||||||
|
'stack-id': args.stack_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
for action in PORTAINER_STACK_CLI_ACTIONS.values():
|
||||||
|
if not (args.action == action['value'] and action['arg_required']):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for arg_name, arg_value in required_args.items():
|
||||||
|
if action['arg_name'] == arg_name and not arg_value:
|
||||||
|
parser.print_help()
|
||||||
|
self.error(f'Argument --{arg_name} is required.')
|
||||||
|
return args
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
parser = ArgumentParser(description=CLI_DESCRIPTION)
|
||||||
|
self.configure_args(parser)
|
||||||
|
self.args = self.process_args(parser)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
host = PortainerHost(PORTAINER_HOST_URLS[self.args.hostname])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cli = PortainerStackCLi()
|
||||||
|
cli.start()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user