mirror of https://github.com/avast/PurpleDome
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
7.5 KiB
Python
243 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# A plugin to control vagrant machines
|
|
|
|
from plugins.base.machinery import MachineryPlugin, MachineStates
|
|
import subprocess
|
|
import vagrant
|
|
from fabric import Connection
|
|
import os
|
|
from app.exceptions import ConfigurationError
|
|
from app.exceptions import NetworkError
|
|
from invoke.exceptions import UnexpectedExit
|
|
import paramiko
|
|
|
|
# Experiment with paramiko instead of fabric. Seems fabric has some issues with the "put" command to Windows. There seems no fix (just my workarounds). Maybe paramiko is better.
|
|
|
|
|
|
class VagrantPlugin(MachineryPlugin):
|
|
|
|
# Boilerplate
|
|
name = "vagrant"
|
|
description = "A plugin for vagrant machines"
|
|
|
|
required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.plugin_path = __file__
|
|
self.v = None
|
|
self.c = None
|
|
self.vagrantfilepath = None
|
|
self.vagrantfile = None
|
|
|
|
def process_config(self, config):
|
|
""" Machine specific processing of configuration """
|
|
|
|
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
|
|
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
|
|
if not os.path.isfile(self.vagrantfile):
|
|
raise ConfigurationError(f"Vagrantfile not existing: {self.vagrantfile}")
|
|
self.v = vagrant.Vagrant(root=self.vagrantfilepath)
|
|
|
|
def create(self, reboot=True):
|
|
""" Create a machine
|
|
|
|
@param reboot: Reboot the VM during installation. Required if you want to install software
|
|
"""
|
|
self.up()
|
|
if reboot:
|
|
self.halt()
|
|
self.up()
|
|
|
|
def up(self):
|
|
""" Start a machine, create it if it does not exist """
|
|
try:
|
|
self.v.up(vm_name=self.config.vmname())
|
|
except subprocess.CalledProcessError as e:
|
|
if self.v.status(vm_name=self.config.vmname())[0].state == self.v.RUNNING:
|
|
return # Everything is fine
|
|
else:
|
|
raise e
|
|
|
|
def halt(self):
|
|
""" Halt a machine """
|
|
forceit = self.config.halt_needs_force()
|
|
try:
|
|
self.v.halt(vm_name=self.config.vmname(), force=forceit)
|
|
except subprocess.CalledProcessError:
|
|
self.v.halt(vm_name=self.config.vmname(), force=True)
|
|
|
|
def destroy(self):
|
|
""" Destroy a machine """
|
|
self.v.destroy(vm_name=self.config.vmname())
|
|
|
|
def connect(self):
|
|
""" Connect to a machine. If there is already a connection we keep it """
|
|
|
|
if self.c:
|
|
return self.c
|
|
|
|
if self.config.os() == "linux":
|
|
uhp = self.v.user_hostname_port(vm_name=self.config.vmname())
|
|
print(f"Connecting to {uhp}")
|
|
self.c = Connection(uhp, connect_kwargs={"key_filename": self.v.keyfile(vm_name=self.config.vmname())})
|
|
print(self.c)
|
|
return self.c
|
|
|
|
if self.config.os() == "windows":
|
|
args = {"key_filename": os.path.join(self.sysconf["abs_machinepath_external"], self.config.ssh_keyfile())}
|
|
if self.config.ssh_password():
|
|
args["password"] = self.config.ssh_password()
|
|
uhp = self.get_ip()
|
|
print(uhp)
|
|
print(args)
|
|
print(self.config.ssh_user())
|
|
# breakpoint()
|
|
self.c = Connection(uhp, user=self.config.ssh_user(), connect_kwargs=args)
|
|
print(self.c)
|
|
return self.c
|
|
|
|
def remote_run(self, cmd, disown=False):
|
|
""" Connects to the machine and runs a command there
|
|
|
|
@param disown: Send the connection into background
|
|
"""
|
|
|
|
if cmd is None:
|
|
return ""
|
|
|
|
self.connect()
|
|
cmd = cmd.strip()
|
|
|
|
print("Vagrant plugin remote run: " + cmd)
|
|
print("Disown: " + str(disown))
|
|
result = None
|
|
retry = 2
|
|
while retry > 0:
|
|
try:
|
|
result = self.c.run(cmd, disown=disown)
|
|
print(result)
|
|
except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit):
|
|
if retry <= 0:
|
|
raise(NetworkError)
|
|
else:
|
|
self.disconnect()
|
|
self.connect()
|
|
retry -= 1
|
|
print("Got some SSH errors. Retrying")
|
|
else:
|
|
break
|
|
|
|
if result and result.stderr:
|
|
print("Debug: Stderr: " + str(result.stderr.strip()))
|
|
|
|
if result:
|
|
return result.stdout.strip()
|
|
|
|
return ""
|
|
|
|
def put(self, src, dst):
|
|
""" Send a file to a machine
|
|
|
|
@param src: source dir
|
|
@param dst: destination
|
|
"""
|
|
self.connect()
|
|
|
|
print(f"{src} -> {dst}")
|
|
|
|
res = ""
|
|
retry = 2
|
|
while retry > 0:
|
|
try:
|
|
res = self.c.put(src, dst)
|
|
except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit):
|
|
if retry <= 0:
|
|
raise (NetworkError)
|
|
else:
|
|
self.disconnect()
|
|
self.connect()
|
|
retry -= 1
|
|
print("Got some SSH errors. Retrying")
|
|
except FileNotFoundError as e:
|
|
print(e)
|
|
break
|
|
else:
|
|
break
|
|
|
|
return res
|
|
|
|
def get(self, src, dst):
|
|
""" Get a file to a machine
|
|
|
|
@param src: source dir
|
|
@param dst: destination
|
|
"""
|
|
self.connect()
|
|
|
|
retry = 2
|
|
while retry > 0:
|
|
try:
|
|
res = self.c.get(src, dst)
|
|
except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit):
|
|
if retry <= 0:
|
|
raise (NetworkError)
|
|
else:
|
|
self.disconnect()
|
|
self.connect()
|
|
retry -= 1
|
|
print("Got some SSH errors. Retrying")
|
|
except FileNotFoundError as e:
|
|
print(e)
|
|
break
|
|
else:
|
|
break
|
|
|
|
return res
|
|
|
|
def disconnect(self):
|
|
""" Disconnect from a machine """
|
|
if self.c:
|
|
self.c.close()
|
|
self.c = None
|
|
|
|
def get_state(self):
|
|
""" Get detailed state of a machine """
|
|
|
|
vstate = self.v.status(vm_name=self.config.vmname())[0].state
|
|
|
|
# mapping vagrant states to PurpleDome states
|
|
mapping = {self.v.RUNNING: MachineStates.RUNNING,
|
|
self.v.NOT_CREATED: MachineStates.NOT_CREATED,
|
|
self.v.POWEROFF: MachineStates.POWEROFF,
|
|
self.v.ABORTED: MachineStates.ABORTED,
|
|
self.v.SAVED: MachineStates.SAVED,
|
|
self.v.STOPPED: MachineStates.STOPPED,
|
|
self.v.FROZEN: MachineStates.FROZEN,
|
|
self.v.SHUTOFF: MachineStates.SHUTOFF
|
|
}
|
|
|
|
return mapping[vstate]
|
|
|
|
def get_ip(self):
|
|
""" Return the machine ip """
|
|
|
|
# TODO: Create special code to extract windows IPs
|
|
|
|
# TODO: Find a smarter way to get the ip
|
|
|
|
# ips = []
|
|
# cmd = "ifconfig"
|
|
# res = self.vm_manager.__call_remote_run__(cmd)
|
|
|
|
# for line in res.split("\n"):
|
|
# m = re.match(r".*inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*", line)
|
|
# if m:
|
|
# print(m.group(1))
|
|
# ips.append(m.group(1))
|
|
|
|
filename = os.path.join(self.sysconf["abs_machinepath_external"], "ip4.txt")
|
|
with open(filename, "rt") as fh:
|
|
return fh.readline().strip()
|