You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1039 lines
41 KiB

# File: vsphere_connector.py
#
# Copyright (c) 2016-2022 Splunk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions
# and limitations under the License.
#
#
# Phantom imports
import os
import re
import ssl
import time
from collections import defaultdict
from tempfile import mkdtemp
from time import mktime
import phantom.app as phantom
import phantom.rules as ph_rules
import requests
from phantom.action_result import ActionResult
from phantom.base_connector import BaseConnector
from phantom.vault import Vault
from pysphere import VIServer
from requests.auth import HTTPBasicAuth
# THIS Connector imports
from vsphere_consts import *
class VsphereConnector(BaseConnector):
# Actions supported by this script
ACTION_ID_GET_REGISTERED_GUESTS = "list_vms"
ACTION_ID_GET_RUNNING_GUESTS = "get_running_guests"
ACTION_ID_START_GUEST = "start_guest"
ACTION_ID_STOP_GUEST = "stop_guest"
ACTION_ID_SUSPEND_GUEST = "suspend_guest"
ACTION_ID_TAKE_SNAPSHOT = "take_snapshot"
ACTION_ID_REVERT_VM = "revert_vm"
ACTION_ID_GET_SYSTEM_INFO = "get_system_info"
def __init__(self):
# Call the BaseConnectors init first
super(VsphereConnector, self).__init__()
self._vs_server = None
self._datacenters = list()
self._verify = False
# Connector result global object
self._vs_server = VIServer()
def initialize(self):
config = self.get_config()
self._verify = config.get('verify_server_cert', False)
# setup the auth
self._auth = HTTPBasicAuth(config[phantom.APP_JSON_USERNAME], config[phantom.APP_JSON_PASSWORD])
self.debug_print('self.status', self.get_status())
return phantom.APP_SUCCESS
def _connect_to_server(self, config):
"""Function that logins to the vsphere server
Args:
config: The json object containing config
Return:
A status code
"""
if self._vs_server.is_connected():
return phantom.APP_SUCCESS
if self._verify is False:
try:
ssl._create_default_https_context = ssl._create_unverified_context
except:
pass
server = config[phantom.APP_JSON_SERVER]
username = config[phantom.APP_JSON_USERNAME]
password = config[phantom.APP_JSON_PASSWORD]
self.save_progress(phantom.APP_PROG_CONNECTING_TO_ELLIPSES, server)
try:
self._vs_server.connect(server, username, password)
except Exception as e:
return self.set_status_save_progress(phantom.APP_ERROR, VSPHERE_ERR_SERVER_CONNECT, e, server_ip=server)
# Get the datacenters
datacenters = self._vs_server.get_datacenters()
self._datacenters = [v for k, v in datacenters.items()]
return phantom.APP_SUCCESS
def _get_system_info(self, config, param):
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# Add the action result
self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))
total_vms = 0
matched = False
ip_hostname = param[VSPHERE_JSON_IP_HOSTNAME]
for datacenter in self._datacenters:
vm_list = self._vs_server.get_registered_vms(datacenter=datacenter)
total_vms += len(vm_list)
for curr_vmx_path in vm_list:
# get the vm object
vm = self._vs_server.get_vm_by_path(curr_vmx_path, datacenter)
ip = vm.get_property('ip_address')
hostname = vm.get_property('hostname')
if (ip_hostname != ip) and (ip_hostname != hostname):
continue
# one of the params matched
curr_data = action_result.add_data({})
curr_data[VSPHERE_JSON_VMX_PATH] = "[{0}]".format(datacenter) + curr_vmx_path
curr_data[phantom.APP_JSON_IP] = ip
curr_data[VSPHERE_JSON_GUEST_NAME] = vm.get_property('name')
curr_data[VSPHERE_JSON_GUEST_HOST_NAME] = hostname
curr_data[VSPHERE_JSON_GUEST_FULL_NAME] = vm.get_property('guest_full_name')
if vm.is_powered_on():
curr_data[phantom.APP_JSON_STATE] = VSPHERE_CONST_VM_STATE_RUNNING
else:
curr_data[phantom.APP_JSON_STATE] = VSPHERE_CONST_VM_STATE_NOT_RUNNING
# Found the endpoint, so break
matched = True
break
# update the summary value about the total guests
action_result.update_summary({'total_vms_searched': total_vms})
action_result.update_summary({'found_endpoint': matched})
action_result.set_status(phantom.APP_SUCCESS)
def _get_vms(self, action, config, param):
"""Function that handles ACTION_ID_GET_REGISTERED_GUESTS and
ACTION_ID_GET_RUNNING_GUESTS
Args:
Return:
A status code
"""
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# Add the action result
self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))
total_vms = 0
total_running = 0
for datacenter in self._datacenters:
if action == self.ACTION_ID_GET_RUNNING_GUESTS:
vm_list = self._vs_server.get_registered_vms(status='poweredOn', datacenter=datacenter)
else:
vm_list = self._vs_server.get_registered_vms(datacenter=datacenter)
total_vms += len(vm_list)
for curr_vmx_path in vm_list:
# get the vm object
vm = self._vs_server.get_vm_by_path(curr_vmx_path, datacenter)
curr_data = action_result.add_data({})
curr_data[VSPHERE_JSON_VMX_PATH] = "[{0}]".format(datacenter) + curr_vmx_path
curr_data[phantom.APP_JSON_IP] = vm.get_property('ip_address')
curr_data[VSPHERE_JSON_GUEST_NAME] = vm.get_property('name')
curr_data[VSPHERE_JSON_GUEST_HOST_NAME] = vm.get_property('hostname')
curr_data[VSPHERE_JSON_GUEST_FULL_NAME] = vm.get_property('guest_full_name')
if vm.is_powered_on():
curr_data[phantom.APP_JSON_STATE] = VSPHERE_CONST_VM_STATE_RUNNING
total_running += 1
else:
curr_data[phantom.APP_JSON_STATE] = VSPHERE_CONST_VM_STATE_NOT_RUNNING
# update the summary value about the total guests
action_result.update_summary({VSPHERE_JSON_TOTAL_GUESTS: total_vms})
action_result.update_summary({VSPHERE_JSON_TOTAL_GUESTS_RUNNING: total_running})
action_result.set_status(phantom.APP_SUCCESS)
def _list_vms(self, action, config, param):
"""Function that handles ACTION_ID_GET_REGISTERED_GUESTS
Args:
Return:
A status code
"""
return self._get_vms(action, config, param)
def _wait_for_async_task(self, task, action, action_result):
"""Function that asynchronously manages the task object
Args:
task: The task object to monitor.
action: The action that is currently being carried out.
action_result: The ActionResult object to hold the status
Return:
A status code of the type phantom.APP_[SUCC|ERR]_XXX.
"""
displayed_once = False
task_name = action.replace('_', ' ')
while True:
# interested in all states
status = task.wait_for_state(['success', 'error', 'queued', 'running'])
if status == 'error':
action_result.set_status(phantom.APP_ERROR, phantom.APP_ERR_CMD_EXEC)
action_result.append_to_message(task.get_error_message())
break
elif status == 'success':
action_result.set_status(phantom.APP_SUCCESS, phantom.APP_SUCC_CMD_EXEC)
break
elif status == 'queued':
self.send_progress(VSPHERE_PROG_TASK_QUEUED)
elif status == 'running':
progress = task.get_progress()
if progress:
self.send_progress(VSPHERE_PROG_TASK_COMPLETED_PERCENT,
task_name=task_name,
progress=progress)
elif not displayed_once:
self.send_progress(VSPHERE_PROG_TASK_RUNNING)
displayed_once = True
time.sleep(2)
return action_result.get_status()
def _parse_vm_path(self, full_vmx_path):
# The full_vmx_path will be of the format
# [datacenter_name][datastore_name] <vm_folder>.<vmname>.vmx
# for e.g. [Datacenter][DAS_labesxi1_1] OpenVAS/OpenVAS.vmx
search = re.search(r"\[(.*)\](\[.*)", full_vmx_path)
if not search:
return VSPHERE_CONST_DEFAULT_DATACENTER, full_vmx_path
datacenter = search.group(1)
vmx_path = search.group(2)
return datacenter, vmx_path
def _handle_start_stop_guest(self, action, config, param):
"""Function that handles ACTION_ID_STOP_GUEST and ACTION_ID_START_GUEST action
Args:
Return:
A status code
"""
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# create an action_result
self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))
# get the config
# The path will contain the datacenter also
datacenter, vmx_path = self._parse_vm_path(param[VSPHERE_JSON_VMX_PATH])
# Get the vm from the path
try:
vm = self._vs_server.get_vm_by_path(vmx_path, datacenter)
except Exception as e:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_VM_FROM_VMX_PATH, e)
task = None
# Only stop if powered on, else the API returns back an error
if (action == self.ACTION_ID_STOP_GUEST) and vm.is_powered_on():
task = vm.power_off(sync_run=False)
# Only start if it can be, else the API returns back an error
elif (action == self.ACTION_ID_START_GUEST) and (not vm.is_powered_on()):
task = vm.power_on(sync_run=False)
if task:
status_code = self._wait_for_async_task(task, action, action_result)
else:
# Most probably we don't have to change the state right now, treat this
# as a success case (debatable)
action_result.set_status(phantom.APP_SUCCESS, VSPHERE_SUCC_CANT_EXEC,
action=action, state=vm.get_status())
return action_result.get_status()
def _handle_start_guest(self, action, config, param):
"""Function that handles ACTION_ID_START_GUEST action
Args:
Return:
A status code
"""
return self._handle_start_stop_guest(action, config, param)
def _handle_stop_guest(self, action, config, param):
"""Function that handles ACTION_ID_STOP_GUEST action
Args:
Return:
A status code
"""
return self._handle_start_stop_guest(action, config, param)
def _create_url_from_path(self, server, vm_file_path, datacenter):
"""Function that creates a url from the path
Args:
server: The ip or machine name of the esx host
vm_file_path: The path of the file, it is expected to be of the format
[<datastore_name>] <folder>/<name>-<random_chars>.<extension>
datacenter: The text that tell which datacenter to use while creating the url
Return:
The url of the file on success.
"""
# it's always in the following format
# [datastore1] WXP3x86/WXP3x86-<random_chars>.<extension>
# print "Got vm_file_path: {}".format(vm_file_path)
# extract the dsName part
start = vm_file_path.find('[') + 1
end = vm_file_path.find(']')
ds_name = vm_file_path[start:end]
# print "dsName: '%s'" % ds_name
# Get the file path after the datastore
start = vm_file_path.find(']') + 2
file_path = vm_file_path[start:]
# now create the query url, which is of the fomat 'https://{}/folder/{}?dcPath={}&dsName={}'
file_url = {}
file_url[VSPHERE_CONST_URL] = 'https://{0}/folder/{1}'.format(server, file_path)
file_url[VSPHERE_CONST_DATACENTER] = datacenter
file_url[VSPHERE_CONST_DATASTORE] = ds_name
return file_url
def _create_url_of_file(self, server, file_type, vm, datacenter, file_name=None):
"""Function that creates a url of a given file that is present on the vm.
Args:
server: The ip or machine name of the esx host
file_type: The file type that must match the type returned using the 'files' property of the vm.
file_name: The file name that must match the name returned using the 'files' property of the vm.
Return:
The url of the file on success, None otherwise.
"""
# get the files for this vm
files = vm.get_property('files', from_cache=False)
# self.debug_print("files data", files)
vm_file = None
for k, v in list(files.items()):
# first match file type
if v['type'] != file_type:
continue
# file type matched
if not file_name:
# no need to match the name, since type matched, file is found
vm_file = v['name']
break
if (file_name) and (v['name'].find(file_name) != -1):
# file_name is specified and it matched
vm_file = v['name']
break
if vm_file is None:
# Not found
return None
return self._create_url_from_path(server, vm_file, datacenter)
def _move_file_to_vault(self, host, container_id, file_size, type_str, local_file_path, result, info, contains):
"""Function that creates a url from the path
Args:
host: The ip or machine name of the esx host
container_id: The container_id to add the file to
vmx_path: The vmx_path of the vm
file_size: Size of the file in bytes that is to be added to vault
type_str: A string representing the type of the file
local_file_path: The local file path that is to be added to the vault
result: The ActionResult object to hold the status
Return:
A status code of the type phantom.APP_[SUCC|ERR]_XXX
"""
self.save_progress(phantom.APP_PROG_ADDING_TO_VAULT)
# lets move the data into the vault
vault_attach_dict = {}
vault_attach_dict[phantom.APP_JSON_HOST] = host
vault_attach_dict[phantom.APP_JSON_INFO] = info
vault_attach_dict[phantom.APP_JSON_CONTAINS] = contains
vault_attach_dict[phantom.APP_JSON_SIZE] = file_size
vault_attach_dict[phantom.APP_JSON_TYPE] = type_str
vault_attach_dict[phantom.APP_JSON_ACTION_NAME] = self.get_action_name()
vault_attach_dict[phantom.APP_JSON_APP_RUN_ID] = self.get_app_run_id()
curr_data = vault_attach_dict
file_name = os.path.basename(local_file_path)
try:
os.chmod(os.path.dirname(local_file_path), 0o770)
success, message, vault_id = ph_rules.vault_add(file_location=local_file_path, container=container_id, file_name=file_name,
metadata=vault_attach_dict)
except Exception as e:
return result.set_status(phantom.APP_ERROR, phantom.APP_ERR_FILE_ADD_TO_VAULT, e)
if success:
curr_data[phantom.APP_JSON_VAULT_ID] = vault_id
curr_data[phantom.APP_JSON_NAME] = file_name
result.add_data(curr_data)
wanted_keys = [phantom.APP_JSON_VAULT_ID, phantom.APP_JSON_NAME, phantom.APP_JSON_SIZE]
summary = dict([ (x, curr_data[x]) for x in wanted_keys if x in curr_data ])
result.update_summary(summary)
result.set_status(phantom.APP_SUCCESS)
else:
result.set_status(phantom.APP_ERROR, phantom.APP_ERR_FILE_ADD_TO_VAULT)
result.append_to_message(message)
return result.get_status()
def _download_file(self, url_to_download, action_result, local_file_path):
"""Function that downloads the file from a url
Args:
url_to_download: the url of the file to download
action_result: The ActionResult object to hold the status
local_file_path: The local file path that was created.
Return:
A status code of the type phantom.APP_[SUCC|ERR]_XXX.
The size in bytes of the file downloaded.
"""
content_size = 0
# Which percent chunks will the download happen for big files
percent_block = 10
# size that sets a file as big.
# A big file will be downloaded in chunks of percent_block else it will be a synchronous download
big_file_size_bytes = 20 * (1024 * 1024)
self.save_progress(phantom.APP_PROG_DOWNLOADING_FILE_FROM_TO,
src=url_to_download[VSPHERE_CONST_URL], dest=local_file_path)
self.debug_print("Complete URL", url_to_download)
# Create the param dictionary
keys = [VSPHERE_CONST_DATACENTER, VSPHERE_CONST_DATASTORE]
params = {x: url_to_download[x] for x in keys}
try:
r = requests.get(url_to_download[VSPHERE_CONST_URL], params=params, verify=self._verify, auth=self._auth, stream=True, timeout=30)
except Exception as e:
return (action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_SERVER_CONNECTION, e), content_size)
if r.status_code != requests.codes.ok: # pylint: disable=E1101
return (action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_SERVER_RETURNED_STATUS_CODE, code=r.status_code), content_size)
# get the content length
content_size = r.headers['content-length']
if not content_size:
return (action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_CANNOT_GET_CONTENT_LENGTH), content_size)
self.save_progress(phantom.APP_PROG_FILE_SIZE, value=content_size, type='bytes')
bytes_to_download = int(content_size)
# init to download the whole file in a single read
block_size = bytes_to_download
# if the file is big then download in % increments
if bytes_to_download > big_file_size_bytes:
block_size = (bytes_to_download * percent_block) / 100
bytes_downloaded = 0
try:
with open(local_file_path, 'wb') as file_handle:
for chunk in r.iter_content(chunk_size=block_size):
if chunk:
bytes_downloaded += len(chunk)
file_handle.write(chunk)
file_handle.flush()
os.fsync(file_handle.fileno())
self.send_progress(VSPHERE_PROG_FINISHED_DOWNLOADING_STATUS, float(bytes_downloaded) / float(bytes_to_download))
except Exception as e:
return (action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_SERVER_CONNECTION, e), content_size)
return (action_result.set_status(phantom.APP_SUCCESS, phantom.APP_SUCC_FILE_DOWNLOAD), content_size)
def _parse_snap_list_file(self, local_file_path, snap_name, id):
"""Function that parses the snapshot list file from a local location and return the file name
Args:
Return:
The file name that for the snapshot with 'snap_name' or None if not found
"""
snap_path = None
snap_list_dict = defaultdict(dict)
with open(local_file_path) as f:
for line in f:
# extract the snapshot index and values that we are interested in
# file name
m = re.search(r'snapshot([0-9]+)\.filename[ ]*=[ ]*"(.*)"', line)
if m:
index = m.group(1)
file_name = m.group(2)
# print "File Name: {} at Index: {}".format(file_name, index)
snap_list_dict[index].update({'key': index, 'file_name': file_name})
# try to see if this is the snapshot we are looking for
# see if the display_Name for this snapshot has been filled in
if VSPHERE_JSON_DISPLAY_NAME in snap_list_dict[index]:
if snap_list_dict[index][VSPHERE_JSON_DISPLAY_NAME] == snap_name:
if id is None:
snap_path = snap_list_dict[index]['file_name']
break
elif 'id' in snap_list_dict[index]:
if snap_list_dict[index]['id'] == id:
snap_path = snap_list_dict[index]['file_name']
break
continue
# display name
m = re.search(r'snapshot([0-9]+)\.displayName[ ]*=[ ]*"(.*)"', line)
if m:
index = m.group(1)
display_name = m.group(2)
# print "Display Name: {} at Index: {}".format(display_name, index)
snap_list_dict[index].update({'key': index, VSPHERE_JSON_DISPLAY_NAME: display_name})
# try to see if this is the snapshot we are looking for
if display_name == snap_name:
# see if the file name for this snapshot has been filled in
if 'file_name' in snap_list_dict[index]:
if id is None:
snap_path = snap_list_dict[index]['file_name']
break
elif 'id' in snap_list_dict[index]:
if snap_list_dict[index]['id'] == id:
snap_path = snap_list_dict[index]['file_name']
break
continue
if id is not None:
m = re.search(r'snapshot([0-9]+)\.uid[ ]*=[ ]*"(.*)"', line)
if m:
index = m.group(1)
snap_id = m.group(2)
# self.debug_print("Snap ID: {} at Index: {}".format(snap_id, index))
snap_list_dict[index].update({'key': index, 'id': snap_id})
# try to see if this is the snapshot we are looking for
if snap_id == id:
# see if the file name for this snapshot has been filled in
if 'file_name' in snap_list_dict[index]:
if VSPHERE_JSON_DISPLAY_NAME in snap_list_dict[index]:
if snap_list_dict[index][VSPHERE_JSON_DISPLAY_NAME] == snap_name:
snap_path = snap_list_dict[index]['file_name']
break
continue
return snap_path
def _download_snapshot_file(self, snap_name, vmx_path, config, vm, action_result, datacenter, id=None):
"""Function that downloads the suspend file from the esx host
Args:
snap_name: The name of the snapshot that is to be downloaded
vmx_path: The vmx_path of the vm
config: The config given to the connector
vm: The pyshpere vm object
action_result: ActionResult object to update the status to
Return:
A status code
"""
# Progress and config
server = config[phantom.APP_JSON_SERVER]
# Downloading the snapshot is a multistep process.
# All we have is a snapshot name, there is no way to query the API
# to map this name to a file on disk (of the esx host) YET.
# So first we need to download the file that contains the snapshot list
# and info about each snapshot.
# Parse this file to map the snapshot name to the file_name on disk
# Then we create a url to the file_name.
# Then download it.
# Create the url to the snapshot list file
snap_list_url = self._create_url_of_file(server, 'snapshotList', vm, datacenter)
if not snap_list_url:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_CANNOT_FIND_SNAPSHOT_LIST_FILE)
# we will be downloading files for this action, so create a tmp folder for it
if hasattr(Vault, 'get_vault_tmp_dir'):
temp_dir = mkdtemp(prefix='vsphere-', dir=Vault.get_vault_tmp_dir())
else:
temp_dir = mkdtemp(prefix='vsphere-', dir='/vault/tmp')
if not os.path.exists(temp_dir):
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_CANNOT_MAKE_TEMP_FOLDER)
# download the file that contains all the snapshots
self.save_progress(VSPHERE_PROG_SNAPSHOT_INFO_DOWNLOADING, snap_name=snap_name)
local_file_path = '{0}/{1}'.format(temp_dir, phantom.get_file_name_from_url(snap_list_url[VSPHERE_CONST_URL]))
status_code, content_size = self._download_file(snap_list_url, action_result, local_file_path)
if phantom.is_fail(status_code):
return action_result.get_status()
# parse the downloaded file and get the file_name that our snapshot represents
snap_file = self._parse_snap_list_file(local_file_path, snap_name, id)
if not snap_file:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_SNAPSHOT_PATH, snap_name)
# got the file name, now get the url of this file_name
snap_file_url = self._create_url_of_file(server, 'snapshotData', vm, datacenter, snap_file)
if not snap_file_url:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_SNAPSHOT_URL, snap_name)
# Set the status to failure, this is to override the successfull download status of the snapshot list file,
action_result.set_status(phantom.APP_ERROR)
os.remove(local_file_path)
local_file_path = '{0}/{1}-{2}'.format(temp_dir,
phantom.get_valid_file_name(phantom.get_valid_file_name(snap_name)),
phantom.get_file_name_from_url(snap_file_url[VSPHERE_CONST_URL]))
self.save_progress(VSPHERE_PROG_SNAPSHOT_DOWNLOADING, snap_name=snap_name)
status_code, content_size = self._download_file(snap_file_url, action_result, local_file_path)
if phantom.is_fail(status_code):
return action_result.get_status()
# move it to the vault
try:
status_code = self._move_file_to_vault(server, self.get_container_id(), content_size, VSPHERE_CONST_SNAPSHOT_FILE_TYPE,
local_file_path, action_result, {VSPHERE_JSON_VMX_PATH: vmx_path}, ["os memory dump", VSPHERE_CONST_SNAPSHOT_FILE_TYPE])
# remove the temp folder
finally:
try:
os.rmdir(temp_dir)
except Exception as e:
self.debug_print('Handled exception', e)
return action_result.get_status()
def _download_suspend_file(self, vmx_path, config, action_result, vm, container_id, datacenter):
"""Function that downloads the suspend file from the esx host
Args:
vmx_path: The vmx_path of the vm
config: The config given to the connector
action_result: ActionResult object to update the status to
vm: The pyshpere vm object
Return:
A status code
"""
# Init the status to failure
action_result.set_status(phantom.APP_ERROR)
# Progress and config
self.save_progress(VSPHERE_PROG_SUSPEND_FILE_DOWNLOADING)
server = config[phantom.APP_JSON_SERVER]
vm_suspend_url = self._create_url_of_file(server, 'suspend', vm, datacenter)
if not vm_suspend_url:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_CANNOT_FIND_SUSPEND_FILE)
# we will be downloading file for this action, so create a tmp folder for it
if hasattr(Vault, 'get_vault_tmp_dir'):
temp_dir = mkdtemp(prefix='vsphere-', dir=Vault.get_vault_tmp_dir())
else:
temp_dir = mkdtemp(prefix='vsphere-', dir='/vault/tmp')
if not os.path.exists(temp_dir):
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_CANNOT_MAKE_TEMP_FOLDER)
# download it
local_file_path = '{0}/{1}'.format(temp_dir, phantom.get_file_name_from_url(vm_suspend_url[VSPHERE_CONST_URL]))
status_code, content_size = self._download_file(vm_suspend_url, action_result, local_file_path)
if phantom.is_fail(status_code):
return action_result.get_status()
# move it to the vault
try:
status_code = self._move_file_to_vault(server, container_id, content_size, VSPHERE_CONST_SUSPEND_FILE_TYPE,
local_file_path, action_result, {VSPHERE_JSON_VMX_PATH: vmx_path}, ["os memory dump", VSPHERE_CONST_SUSPEND_FILE_TYPE])
finally:
try:
# remove the temp folder
os.rmdir(temp_dir)
except Exception as e:
self.debug_print('Handled exception', e)
return action_result.get_status()
def _get_latest_snapshot_info(self, vm):
snapshot_list = vm.get_snapshots()
last_snap_time = 0
last_snap = None
for snapshot in snapshot_list:
create_time = snapshot.get_create_time()
create_time_secs = mktime(create_time)
if last_snap_time < create_time_secs:
last_snap_time = create_time_secs
last_snap = snapshot
if last_snap:
self.debug_print('last create time epoch', last_snap_time)
self.debug_print('last snap name', last_snap.get_name())
name = last_snap.get_name()
id = None
obj_string = last_snap.properties._obj
m = re.search('.*snapshot-([0-9]+)', obj_string)
if m:
id = m.group(1)
return name, id
return None, None
def _handle_take_snapshot(self, action, config, param, container_id):
"""Function that handles ACTION_ID_TAKE_SNAPSHOT
Args:
Return:
A status code
"""
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# create an action_result to represent this item
action_result = self.add_action_result(ActionResult(dict(param)))
# The path will contain the datacenter also
datacenter, vmx_path = self._parse_vm_path(param[VSPHERE_JSON_VMX_PATH])
self.debug_print("datacenter", datacenter)
self.debug_print("vmx_path", vmx_path)
# Get the vm object from the vmx path
try:
vm = self._vs_server.get_vm_by_path(vmx_path, datacenter)
except Exception as e:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_VM_FROM_VMX_PATH, e)
snap_name = VSPHERE_CONST_SNAPSHOT_NAME_PREFIX + phantom.get_random_chars()
snap_desc = VSPHERE_CONST_SNAPSHOT_DESCRIPTION.format(container_id=self.get_container_id())
self.save_progress(VSPHERE_PROG_SNAPSHOT_NAME, snap_name=snap_name)
task = vm.create_snapshot(snap_name, description=snap_desc, memory=True, sync_run=False)
status_code = self._wait_for_async_task(task, action, action_result)
state_not_changed = False
if action_result.get_message().find(VSPHERE_VIRTUAL_MACHINE_NOT_CHANGED) != -1:
state_not_changed = True
if phantom.is_fail(status_code) and (not state_not_changed):
return status_code
else:
message = VSPHERE_VIRTUAL_MACHINE_NOT_CHANGED if (state_not_changed) else VSPHERE_PROG_SNAPSHOT_TAKEN
self.save_progress(message)
action_result.set_status(phantom.APP_SUCCESS, message)
# now check if it needs to be downloaded
download = bool(param[phantom.APP_JSON_DOWNLOAD]) if (phantom.APP_JSON_DOWNLOAD in param) else True
if download:
# set the action result to failure
self.set_status(phantom.APP_ERROR)
id = None
if state_not_changed:
snap_name, id = self._get_latest_snapshot_info(vm)
if not snap_name or not id:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_FAILED_TO_GET_SNAPSHOT_INFO)
self.debug_print("Latest snapshot: {0} with id {1}".format(snap_name, id))
status_code = self._download_snapshot_file(snap_name, vmx_path, config, vm, action_result, datacenter, id)
return action_result.get_status()
def _revert_vm(self, action, config, param):
""""""
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# create an action_result to represent this item
self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))
# The path will contain the datacenter also
datacenter, vmx_path = self._parse_vm_path(param[VSPHERE_JSON_VMX_PATH])
# Get the vm object from the vmx path
try:
vm = self._vs_server.get_vm_by_path(vmx_path, datacenter)
except Exception as e:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_VM_FROM_VMX_PATH, e)
# get the snapshot name
snap_name = param.get(VSPHERE_JSON_SNAP_NAME)
try:
if snap_name is not None:
task = vm.revert_to_named_snapshot(snap_name, sync_run=False)
else:
task = vm.revert_to_snapshot(sync_run=False)
status_code = self._wait_for_async_task(task, action, action_result)
except Exception as e:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_FAILED_TO_REVERT_VM, err_msg=str(e))
if phantom.is_fail(status_code):
return status_code
else:
action_result.set_status(phantom.APP_SUCCESS, phantom.APP_SUCC_CMD_EXEC)
self.save_progress(VSPHERE_PROG_SNAPSHOT_REVERTED)
return action_result.get_status()
def _handle_suspend_guest(self, action, config, param, container_id):
"""Function that handles ACTION_ID_SUSPEND_GUEST
Also downloads the file if config tells it to
Args:
Return:
A status code
"""
# Connect to the server
status_code = self._connect_to_server(config)
if phantom.is_fail(status_code):
return status_code
# create an action_result to represent this item
action_result = self.add_action_result(ActionResult(dict(param)))
# The path will contain the datacenter also
datacenter, vmx_path = self._parse_vm_path(param[VSPHERE_JSON_VMX_PATH])
# Get the vm object from the vmx path
try:
vm = self._vs_server.get_vm_by_path(vmx_path, datacenter)
except Exception as e:
return action_result.set_status(phantom.APP_ERROR, VSPHERE_ERR_VM_FROM_VMX_PATH, e)
task = None
# Only suspend if not suspended
if not vm.is_suspended():
task = vm.suspend(sync_run=False)
status_code = self._wait_for_async_task(task, action, action_result)
if phantom.is_fail(status_code):
return status_code
else:
action_result.set_status(phantom.APP_SUCCESS, phantom.APP_SUCC_CMD_EXEC)
self.save_progress(VSPHERE_PROG_SUSPENDED)
else:
# Set the status to havent suspended, if file is to be downloaded then it will be overwritten
self.save_progress(VSPHERE_PROG_SKIPPING_SUSPEND, state=vm.get_status())
action_result.set_status(phantom.APP_SUCCESS, VSPHERE_SUCC_CANT_EXEC, action=action, state=vm.get_status())
# either the vm was already suspended or we were able to do it now check if it needs to be downloaded
download = param[phantom.APP_JSON_DOWNLOAD] if (phantom.APP_JSON_DOWNLOAD in param) else False
if download:
status_code = self._download_suspend_file(vmx_path, config, action_result, vm, container_id, datacenter)
return action_result.get_status()
def _test_asset_connectivity(self, config, param):
if phantom.is_fail(self._connect_to_server(config)):
self.debug_print("connect failed")
self.save_progress(VSPHERE_ERR_CONNECTIVITY_TEST)
return self.append_to_message(VSPHERE_ERR_CONNECTIVITY_TEST)
self.debug_print("connect passed")
self.save_progress(VSPHERE_SUCC_CONNECTIVITY_TEST)
return self.set_status(phantom.APP_SUCCESS, VSPHERE_SUCC_CONNECTIVITY_TEST)
def handle_action(self, param):
"""
"""
result = None
action = self.get_action_identifier()
config = self.get_config()
container_id = self.get_container_id()
if (action == self.ACTION_ID_GET_REGISTERED_GUESTS) or (action == self.ACTION_ID_GET_RUNNING_GUESTS):
result = self._get_vms(action, config, param)
elif (action == self.ACTION_ID_START_GUEST):
result = self._handle_start_guest(action, config, param)
elif (action == self.ACTION_ID_STOP_GUEST):
result = self._handle_stop_guest(action, config, param)
elif(action == self.ACTION_ID_SUSPEND_GUEST):
result = self._handle_suspend_guest(action, config, param, container_id)
elif (action == self.ACTION_ID_TAKE_SNAPSHOT):
result = self._handle_take_snapshot(action, config, param, container_id)
elif (action == self.ACTION_ID_REVERT_VM):
result = self._revert_vm(action, config, param)
elif (action == self.ACTION_ID_GET_SYSTEM_INFO):
result = self._get_system_info(config, param)
elif (action == phantom.ACTION_ID_TEST_ASSET_CONNECTIVITY):
result = self._test_asset_connectivity(config, param)
# clean it up
self._vs_server.disconnect()
return result
def handle_exception(self, exception):
"""
"""
# This throws an exception sometimes stating that server is not connected
# even when is_connected returns True, could happen if the remote end disconnects.
try:
if self._vs_server.is_connected():
self._vs_server.disconnect()
except:
pass
if __name__ == '__main__':
import json
import sys
import pudb
pudb.set_trace()
if (len(sys.argv) < 2):
print("No test json specified as input")
sys.exit(0)
with open(sys.argv[1]) as f:
in_json = f.read()
in_json = json.loads(in_json)
print(json.dumps(in_json, indent=4))
connector = VsphereConnector()
connector.print_progress_message = True
ret_val = connector._handle_action(json.dumps(in_json), None)
print(json.dumps(json.loads(ret_val), indent=4))
sys.exit(0)