diff --git a/src/main.py b/src/main.py
index d524b2a..53c428c 100644
--- a/src/main.py
+++ b/src/main.py
@@ -8,6 +8,7 @@ import string
import sys
import textwrap
import time
+from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from getpass import getpass
from pathlib import Path
@@ -30,9 +31,30 @@ workdir = Path.home() / ".vmker"
logdir = workdir / "logs"
os.makedirs(logdir / "by-new_name", exist_ok=True)
logger.remove()
-logger.add(sys.stdout, level="INFO", backtrace=False, diagnose=False,
- format="\r{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}")
logger.add(logdir / f"debug-{datetime.now().strftime('%d.%m.%Y_%H-%M.%S')}.log", level=0)
+
+_argv = sys.argv[1:]
+args = {}
+i = 0
+while i < len(_argv):
+ arg = _argv[i]
+ if arg.startswith('-'):
+ key = arg[1:]
+ if i + 1 < len(_argv) and not _argv[i + 1].startswith('-'):
+ if not args.get(key):
+ args[key] = []
+ args[key].append(_argv[i + 1])
+ i += 1
+ else:
+ logger.warning(f"Ignored invalid parameter: {key}")
+ i += 1
+
+if args.get("debug"):
+ logger.add(sys.stdout, level=0, backtrace=False, diagnose=False,
+ format="\r{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}")
+else:
+ logger.add(sys.stdout, level="INFO", backtrace=False, diagnose=False,
+ format="\r{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}")
logger.debug("Logger initialized")
@@ -94,10 +116,13 @@ class VMKer:
def _load_config(self):
cf = workdir / "config.json"
+ if args.get('c'):
+ cf = Path().resolve() / args['c'][0]
+ logger.debug(f"using config file: {cf}")
if not cf.exists():
logger.info(f"Init before using program: {sys.argv[0]} init")
exit(0)
- with open(workdir / "config.json", "r") as f:
+ with open(cf, "r") as f:
self.config = json.load(f)
def _connect(self):
@@ -255,17 +280,22 @@ class VMKer:
pass
def _clone(self, new_name, destfolder, clone_spec):
- logger.info(f"Cloning: {self.template.name!r} > {new_name!r}")
+ logger.info(f"[{new_name}] Cloning from {self.template.name!r}")
task = self.template.Clone(name=new_name, folder=destfolder, spec=clone_spec)
while task.info.state == vim.TaskInfo.State.running:
time.sleep(1)
if task.info.state != vim.TaskInfo.State.success:
logger.error(f"Error while cloning")
logger.exception(task.info.error)
- logger.success("Cloned")
- logger.info("Waiting IP VirtualMachine..")
- vm = task.info.result
+ logger.success(f"[{new_name}] Cloned")
+ return task.info.result
+ def _configure(self, vm):
+ _name = vm.name
+ user = 'root'
+ key_file = 'rsa'
+
+ logger.info("Waiting VirtualMachine..")
t = 0
while True:
ipv4, ipv6 = self._get_vm_ip(vm)
@@ -276,8 +306,8 @@ class VMKer:
if t > 120:
return logger.error("No IPv4 address found.")
- logger.info(f"IPv4: {', '.join(ipv4)}")
- logger.info(f"IPv6: {', '.join(ipv6)}")
+ logger.info(f"[{_name}] IPv4: {', '.join(ipv4)}")
+ logger.info(f"[{_name}] IPv6: {', '.join(ipv6)}")
try:
# TODO: Default user
@@ -285,20 +315,21 @@ class VMKer:
ssh = ConnectHandler(
device_type='linux',
host=ipv4[0],
- username="root",
+ username=user,
use_keys=True,
- key_file="rsa"
+ key_file=key_file
)
except netmiko.exceptions.NetmikoAuthenticationException as e:
return logger.error(f"SSH Error: Authentication. ({e})")
+ logger.success(f"[SSH] [{_name}] Connected to {ipv4[0]} as {user} ({key_file=})")
# TODO: length
_pwd = ''.join(random.choice(chars) for _ in range(12))
- ssh.send_command(f'echo -e "{_pwd}\n{_pwd}" | passwd root')
+ ssh.send_command(f"echo '{user}:{_pwd}' | chpasswd")
logger.info(f"Password for root: {_pwd}")
- ssh.send_command(f'echo {new_name} > /etc/hostname')
+ ssh.send_command(f'echo {_name} > /etc/hostname')
# TODO: yml
- logger.info("Executing %.yml")
+ logger.info(f"[{vm.name}] Executing %.yml")
ssh.disconnect()
@@ -319,13 +350,13 @@ class VMKer:
if len(sys.argv) > 3:
new_name = sys.argv[3]
template_name = f"{sys.argv[2]}-template"
- _logfile = logdir / "by-new_vm_name" / f"{new_name}.log"
+ _logfile = logdir / "by-new_name" / f"{new_name}.log"
i = 0
while True:
i += 1
if not _logfile.exists():
break
- _logfile = logdir / "by-new_vm_name" / f"{new_name} ({i}).log"
+ _logfile = logdir / "by-new_name" / f"{new_name} ({i}).log"
logger.add(_logfile, level=0)
self._load_config()
if not self._connect():
@@ -339,7 +370,10 @@ class VMKer:
logger.debug("datacenter found")
self._cache_vms(template_name, datacenter)
- new_names = [f'{new_name}-{i+1}' for i in range(1)]
+ if args.get("n"):
+ new_names = [f'{new_name}-{i + 1}' for i in range(int(args['n'][0]))]
+ else:
+ new_names = [new_name]
for n in new_names:
if n in self.vm:
return logger.error(f"Error: {n!r} already exist.")
@@ -372,15 +406,23 @@ class VMKer:
clone_spec = vim.vm.CloneSpec()
clone_spec.location = relocate_spec
clone_spec.powerOn = True
- for n in new_names:
- self._clone(n, destfolder, clone_spec)
+
+ def _caf(nn):
+ vm = self._clone(nn, destfolder, clone_spec)
+ self._configure(vm)
+
+ for new_name in new_names:
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ executor.submit(_caf, new_name)
+
else:
logger.info(
- f"Usage: {sys.argv[0]} create [commands.yml | commands1.yml commands2.yml | ...]")
+ f"Usage: {sys.argv[0]} create template new [-n VMs count] [[-f ] ...]")
logger.info("Mark: template search on target host with '-template' suffix (%template_name%-template)")
def main(self):
logger.debug(f"{sys.argv=}")
+ logger.debug(f"{args=}")
try:
if len(sys.argv) > 1:
match sys.argv[1]:
@@ -400,19 +442,5 @@ class VMKer:
logger.info("Exited by KeyboardInterrupt")
-def generate_password(length=12):
- chars = string.ascii_letters + string.digits + string.punctuation
- return ''.join(random.choice(chars) for _ in range(length))
-
-
-def get_vm_ip(vm):
- for nic in vm.guest.net:
- if nic.ipConfig:
- for ip in nic.ipConfig.ipAddress:
- if ":" not in ip.ipAddress: # Skip IPv6 addresses
- return ip.ipAddress
- return None
-
-
if __name__ == "__main__":
VMKer().main()