modules.machinery.vsphere

Package modules :: Package machinery :: Module vsphere |
[hide private]
[frames] | no frames]

# Source Code for Module modules.machinery.vsphere

      1  # Copyright (C) 2015 eSentire, Inc (jacob.gajek@esentire.com).
      2  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
      3  # See the file 'docs/LICENSE' for copying permission.
      4
      5  import datetime
      6  import logging
      7  import random
      8  import re
      9  import requests
     10  import ssl
     11  import time
     12
     13  from lib.cuckoo.common.abstracts import Machinery
     14  from lib.cuckoo.common.exceptions import CuckooMachineError
     15  from lib.cuckoo.common.exceptions import CuckooDependencyError
     16  from lib.cuckoo.common.exceptions import CuckooCriticalError
     17
     18  try:
     19      from pyVim.connect import SmartConnection
     20      HAVE_PYVMOMI = True
     21  except ImportError:
     22      HAVE_PYVMOMI = False
     23
     24  log = logging.getLogger(__name__)
     25  logging.getLogger("requests").setLevel(logging.WARNING)
     26
     27
     28 -class vSphere(Machinery):
     29      """vSphere/ESXi machinery class based on pyVmomi Python SDK."""
     30
     31      # VM states
     32      RUNNING = "poweredOn"
     33      POWEROFF = "poweredOff"
     34      SUSPENDED = "suspended"
     35      ABORTED = "aborted"
     36
     37 -    def __init__(self):
     38          if not HAVE_PYVMOMI:
     39              raise CuckooDependencyError(
     40                  "Couldn't import pyVmomi, please install it (using "
     41                  "`pip install -U pyvmomi`)"
     42              )
     43
     44          super(vSphere, self).__init__()
     45
     46 -    def _initialize(self, module_name):
     47          """Read configuration.
     48          @param module_name: module name.
     49          """
     50          super(vSphere, self)._initialize(module_name)
     51
     52          # Initialize random number generator
     53          random.seed()
     54
     55 -    def _initialize_check(self):
     56          """Runs checks against virtualization software when a machine manager
     57          is initialized.
     58          @raise CuckooCriticalError: if a misconfiguration or unsupported state
     59                                      is found.
     60          """
     61          self.connect_opts = {}
     62
     63          if self.options.vsphere.host:
     64              self.connect_opts["host"] = self.options.vsphere.host
     65          else:
     66              raise CuckooCriticalError(
     67                  "vSphere host address setting not found, please add it "
     68                  "to the config file."
     69              )
     70
     71          if self.options.vsphere.port:
     72              self.connect_opts["port"] = self.options.vsphere.port
     73          else:
     74              raise CuckooCriticalError(
     75                  "vSphere port setting not found, please add it to the "
     76                  "config file."
     77              )
     78
     79          if self.options.vsphere.user:
     80              self.connect_opts["user"] = self.options.vsphere.user
     81          else:
     82              raise CuckooCriticalError(
     83                  "vSphere username setting not found, please add it to "
     84                  "the config file."
     85              )
     86
     87          if self.options.vsphere.pwd:
     88              self.connect_opts["pwd"] = self.options.vsphere.pwd
     89          else:
     90              raise CuckooCriticalError(
     91                  "vSphere password setting not found, please add it to "
     92                  "the config file."
     93              )
     94
     95          # Workaround for PEP-0476 issues in recent Python versions
     96          if self.options.vsphere.unverified_ssl:
     97              sslContext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
     98              sslContext.verify_mode = ssl.CERT_NONE
     99              self.connect_opts["sslContext"] = sslContext
    100              log.warn("Turning off SSL certificate verification!")
    101
    102          # Check that a snapshot is configured for each machine
    103          # and that it was taken in a powered-on state
    104          try:
    105              with SmartConnection(**self.connect_opts) as conn:
    106                  for machine in self.machines():
    107                      if not machine.snapshot:
    108                          raise CuckooCriticalError(
    109                              "Snapshot name not specified for machine %s, "
    110                              "please add it to the config file." %
    111                              machine.label
    112                          )
    113
    114                      vm = self._get_virtual_machine_by_label(conn, machine.label)
    115                      if not vm:
    116                          raise CuckooCriticalError(
    117                              "Unable to find machine %s on vSphere host, "
    118                              "please update your configuration." %
    119                              machine.label
    120                          )
    121
    122                      state = self._get_snapshot_power_state(vm, machine.snapshot)
    123                      if not state:
    124                          raise CuckooCriticalError(
    125                              "Unable to find snapshot %s for machine %s, "
    126                              "please update your configuration." %
    127                              (machine.snapshot, machine.label)
    128                          )
    129
    130                      if state != self.RUNNING:
    131                          raise CuckooCriticalError(
    132                              "Snapshot for machine %s not in powered-on "
    133                              "state, please create one." % machine.label
    134                          )
    135          except CuckooCriticalError as e:
    136              raise e
    137          except Exception as e:
    138              raise CuckooCriticalError(
    139                  "Couldn't connect to vSphere host: %s" % e
    140              )
    141
    142          super(vSphere, self)._initialize_check()
    143
    144 -    def start(self, label, task):
    145          """Start a machine.
    146          @param label: machine name.
    147          @param task: task object.
    148          @raise CuckooMachineError: if unable to start machine.
    149          """
    150          name = self.db.view_machine_by_label(label).snapshot
    151          with SmartConnection(**self.connect_opts) as conn:
    152              vm = self._get_virtual_machine_by_label(conn, label)
    153              if vm:
    154                  self._revert_snapshot(vm, name)
    155              else:
    156                  raise CuckooMachineError(
    157                      "Machine %s not found on host" % label
    158                  )
    159
    160 -    def stop(self, label):
    161          """Stop a machine.
    162          @param label: machine name.
    163          @raise CuckooMachineError: if unable to stop machine
    164          """
    165          with SmartConnection(**self.connect_opts) as conn:
    166              vm = self._get_virtual_machine_by_label(conn, label)
    167              if vm:
    168                  self._stop_virtual_machine(vm)
    169              else:
    170                  raise CuckooMachineError(
    171                      "Machine %s not found on host" % label
    172                  )
    173
    174 -    def dump_memory(self, label, path):
    175          """Take a memory dump of a machine.
    176          @param path: path to where to store the memory dump
    177          @raise CuckooMachineError: if error taking the memory dump
    178          """
    179          name = "cuckoo_memdump_{0}".format(random.randint(100000, 999999))
    180          with SmartConnection(**self.connect_opts) as conn:
    181              vm = self._get_virtual_machine_by_label(conn, label)
    182              if vm:
    183                  self._create_snapshot(vm, name)
    184                  self._download_snapshot(conn, vm, name, path)
    185                  self._delete_snapshot(vm, name)
    186              else:
    187                  raise CuckooMachineError(
    188                      "Machine %s not found on host" % label
    189                  )
    190
    191 -    def _list(self):
    192          """List virtual machines on vSphere host"""
    193          ret = []
    194          with SmartConnection(**self.connect_opts) as conn:
    195              for vm in self._get_virtual_machines(conn):
    196                  ret.append(vm.summary.config.name)
    197          return ret
    198
    199 -    def _status(self, label):
    200          """Get power state of vm from vSphere host.
    201          @param label: virtual machine name
    202          @raise CuckooMachineError: if error getting status or machine not found
    203          """
    204          with SmartConnection(**self.connect_opts) as conn:
    205              vm = self._get_virtual_machine_by_label(conn, label)
    206              if not vm:
    207                  raise CuckooMachineError(
    208                      "Machine %s not found on server" % label
    209                  )
    210
    211              status = vm.runtime.powerState
    212              self.set_status(label, status)
    213              return status
    214
    215 -    def _get_virtual_machines(self, conn):
    216          """Iterate over all VirtualMachine managed objects on vSphere host"""
    217          def traverseDCFolders(conn, nodes, path=""):
    218              for node in nodes:
    219                  if hasattr(node, "childEntity"):
    220                      for child, childpath in traverseDCFolders(conn, node.childEntity, path + node.name + "/"):
    221                          yield child, childpath
    222                  else:
    223                      yield node, path + node.name
    224
    225          def traverseVMFolders(conn, nodes):
    226              for node in nodes:
    227                  if hasattr(node, "childEntity"):
    228                      for child in traverseVMFolders(conn, node.childEntity):
    229                          yield child
    230                  else:
    231                      yield node
    232
    233          self.VMtoDC = {}
    234
    235          for dc, dcpath in traverseDCFolders(conn, conn.content.rootFolder.childEntity):
    236              for vm in traverseVMFolders(conn, dc.vmFolder.childEntity):
    237                  self.VMtoDC[vm.summary.config.name] = dcpath
    238                  yield vm
    239
    240 -    def _get_virtual_machine_by_label(self, conn, label):
    241          """Return the named VirtualMachine managed object"""
    242          for vm in self._get_virtual_machines(conn):
    243              if vm.summary.config.name == label:
    244                  return vm
    245
    246 -    def _get_snapshot_by_name(self, vm, name):
    247          """Return the named VirtualMachineSnapshot managed object for
    248          a virtual machine"""
    249          for ss in self._traverseSnapshots(vm.snapshot.rootSnapshotList):
    250              if ss.name == name:
    251                  return ss.snapshot
    252
    253 -    def _get_snapshot_power_state(self, vm, name):
    254          """Return the power state for a named VirtualMachineSnapshot object"""
    255          for ss in self._traverseSnapshots(vm.snapshot.rootSnapshotList):
    256              if ss.name == name:
    257                  return ss.state
    258
    259 -    def _create_snapshot(self, vm, name):
    260          """Create named snapshot of virtual machine"""
    261          log.info(
    262              "Creating snapshot %s for machine %s",
    263              name, vm.summary.config.name
    264          )
    265
    266          task = vm.CreateSnapshot_Task(name=name,
    267                                        description="Created by Cuckoo sandbox",
    268                                        memory=True,
    269                                        quiesce=False)
    270          try:
    271              self._wait_task(task)
    272          except CuckooMachineError as e:
    273              raise CuckooMachineError("CreateSnapshot: %s" % e)
    274
    275 -    def _delete_snapshot(self, vm, name):
    276          """Remove named snapshot of virtual machine"""
    277          snapshot = self._get_snapshot_by_name(vm, name)
    278          if snapshot:
    279              log.info(
    280                  "Removing snapshot %s for machine %s",
    281                  name, vm.summary.config.name
    282              )
    283
    284              task = snapshot.RemoveSnapshot_Task(removeChildren=True)
    285              try:
    286                  self._wait_task(task)
    287              except CuckooMachineError as e:
    288                  log.error("RemoveSnapshot: {0}".format(e))
    289          else:
    290              raise CuckooMachineError(
    291                  "Snapshot %s for machine %s not found" %
    292                  (name, vm.summary.config.name)
    293              )
    294
    295 -    def _revert_snapshot(self, vm, name):
    296          """Revert virtual machine to named snapshot"""
    297          snapshot = self._get_snapshot_by_name(vm, name)
    298          if snapshot:
    299              log.info(
    300                  "Reverting machine %s to snapshot %s",
    301                  vm.summary.config.name, name
    302              )
    303
    304              task = snapshot.RevertToSnapshot_Task()
    305              try:
    306                  self._wait_task(task)
    307              except CuckooMachineError as e:
    308                  raise CuckooMachineError("RevertToSnapshot: %s" % e)
    309          else:
    310              raise CuckooMachineError(
    311                  "Snapshot %s for machine %s not found" %
    312                  (name, vm.summary.config.name)
    313              )
    314
    315 -    def _download_snapshot(self, conn, vm, name, path):
    316          """Download snapshot file from host to local path"""
    317
    318          # Get filespec to .vmsn or .vmem file of named snapshot
    319          snapshot = self._get_snapshot_by_name(vm, name)
    320          if not snapshot:
    321              raise CuckooMachineError(
    322                  "Snapshot %s for machine %s not found" %
    323                  (name, vm.summary.config.name)
    324              )
    325
    326          memorykey = datakey = filespec = None
    327          for s in vm.layoutEx.snapshot:
    328              if s.key == snapshot:
    329                  memorykey = s.memoryKey
    330                  datakey = s.dataKey
    331                  break
    332
    333          for f in vm.layoutEx.file:
    334              if f.key == memorykey and (f.type == "snapshotMemory" or
    335                                         f.type == "suspendMemory"):
    336                  filespec = f.name
    337                  break
    338
    339          if not filespec:
    340              for f in vm.layoutEx.file:
    341                  if f.key == datakey and f.type == "snapshotData":
    342                      filespec = f.name
    343                      break
    344
    345          if not filespec:
    346              raise CuckooMachineError("Could not find snapshot memory file")
    347
    348          log.info("Downloading memory dump %s to %s", filespec, path)
    349
    350          # Parse filespec to get datastore and file path.
    351          datastore, filepath = re.match(r"\[([^\]]*)\] (.*)", filespec).groups()
    352
    353          # Construct URL request
    354          params = {
    355              "dsName": datastore,
    356              "dcPath": self.VMtoDC.get(vm.summary.config.name, "ha-datacenter")
    357          }
    358          headers = {
    359              "Cookie": conn._stub.cookie,
    360          }
    361          url = "https://%s:%s/folder/%s" % (
    362              self.connect_opts["host"], self.connect_opts["port"], filepath
    363          )
    364
    365          # Stream download to specified local path
    366          try:
    367              response = requests.get(url, params=params, headers=headers,
    368                                      verify=False, stream=True)
    369
    370              response.raise_for_status()
    371
    372              with open(path, "wb") as localfile:
    373                  for chunk in response.iter_content(16*1024):
    374                      localfile.write(chunk)
    375
    376          except Exception as e:
    377              raise CuckooMachineError(
    378                  "Error downloading memory dump %s: %s" %
    379                  (filespec, e)
    380              )
    381
    382 -    def _stop_virtual_machine(self, vm):
    383          """Power off a virtual machine"""
    384          log.info("Powering off virtual machine %s", vm.summary.config.name)
    385          task = vm.PowerOffVM_Task()
    386          try:
    387              self._wait_task(task)
    388          except CuckooMachineError as e:
    389              log.error("PowerOffVM: %s", e)
    390
    391 -    def _wait_task(self, task):
    392          """Wait for a task to complete with timeout"""
    393          limit = datetime.timedelta(
    394              seconds=int(self.options_globals.timeouts.vm_state)
    395          )
    396          start = datetime.datetime.utcnow()
    397
    398          while True:
    399              if task.info.state == "error":
    400                  raise CuckooMachineError("Task error")
    401
    402              if task.info.state == "success":
    403                  break
    404
    405              if datetime.datetime.utcnow() - start > limit:
    406                  raise CuckooMachineError("Task timed out")
    407
    408              time.sleep(1)
    409
    410 -    def _traverseSnapshots(self, root):
    411          """Recursive depth-first traversal of snapshot tree"""
    412          for node in root:
    413              if len(node.childSnapshotList) > 0:
    414                  for child in self._traverseSnapshots(node.childSnapshotList):
    415                      yield child
    416              yield node
    417