| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You 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. |
| |
| """ |
| VMware vSphere driver. Uses pyvmomi - https://github.com/vmware/pyvmomi |
| Code inspired by https://github.com/vmware/pyvmomi-community-samples |
| |
| Authors: Dimitris Moraitis, Alex Tsiliris, Markos Gogoulos |
| """ |
| |
| import ssl |
| import json |
| import time |
| import atexit |
| import base64 |
| import asyncio |
| import hashlib |
| import logging |
| import warnings |
| import functools |
| import itertools |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.common.base import JsonResponse, ConnectionKey |
| from libcloud.common.types import LibcloudError, ProviderError, InvalidCredsError |
| from libcloud.compute.base import Node, NodeSize, NodeImage, NodeDriver, NodeLocation |
| from libcloud.compute.types import Provider, NodeState |
| from libcloud.utils.networking import is_public_subnet |
| from libcloud.common.exceptions import BaseHTTPError |
| |
| try: |
| from pyVim import connect |
| from pyVmomi import VmomiSupport, vim, vmodl |
| from pyVim.task import WaitForTask |
| |
| pyvmomi = True |
| except ImportError: |
| pyvmomi = None |
| |
| |
| logger = logging.getLogger("libcloud.compute.drivers.vsphere") |
| |
| |
| def recurse_snapshots(snapshot_list): |
| ret = [] |
| for s in snapshot_list: |
| ret.append(s) |
| ret += recurse_snapshots(getattr(s, "childSnapshotList", [])) |
| return ret |
| |
| |
| def format_snapshots(snapshot_list): |
| ret = [] |
| for s in snapshot_list: |
| ret.append( |
| { |
| "id": s.id, |
| "name": s.name, |
| "description": s.description, |
| "created": s.createTime.strftime("%Y-%m-%d %H:%M"), |
| "state": s.state, |
| } |
| ) |
| return ret |
| |
| |
| # 6.5 and older, probably won't work on anything earlier than 4.x |
| class VSphereNodeDriver(NodeDriver): |
| name = "VMware vSphere" |
| website = "http://www.vmware.com/products/vsphere/" |
| type = Provider.VSPHERE |
| |
| NODE_STATE_MAP = { |
| "poweredOn": NodeState.RUNNING, |
| "poweredOff": NodeState.STOPPED, |
| "suspended": NodeState.SUSPENDED, |
| } |
| |
| def __init__(self, host, username, password, port=443, ca_cert=None): |
| """Initialize a connection by providing a hostname, |
| username and password |
| """ |
| if pyvmomi is None: |
| raise ImportError( |
| 'Missing "pyvmomi" dependency. ' |
| "You can install it " |
| "using pip - pip install pyvmomi" |
| ) |
| self.host = host |
| try: |
| if ca_cert is None: |
| self.connection = connect.SmartConnect( |
| host=host, |
| port=port, |
| user=username, |
| pwd=password, |
| ) |
| else: |
| context = ssl.create_default_context(cafile=ca_cert) |
| self.connection = connect.SmartConnect( |
| host=host, |
| port=port, |
| user=username, |
| pwd=password, |
| sslContext=context, |
| ) |
| atexit.register(connect.Disconnect, self.connection) |
| except Exception as exc: |
| error_message = str(exc).lower() |
| if "incorrect user name" in error_message: |
| raise InvalidCredsError("Check your username and " "password are valid") |
| if "connection refused" in error_message or "is not a vim server" in error_message: |
| raise LibcloudError( |
| "Check that the host provided is a " "vSphere installation", |
| driver=self, |
| ) |
| if "name or service not known" in error_message: |
| raise LibcloudError("Check that the vSphere host is accessible", driver=self) |
| if "certificate verify failed" in error_message: |
| # bypass self signed certificates |
| try: |
| context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) |
| context.verify_mode = ssl.CERT_NONE |
| except ImportError: |
| raise ImportError( |
| "To use self signed certificates, " |
| "please upgrade to python 2.7.11 and " |
| "pyvmomi 6.0.0+" |
| ) |
| |
| self.connection = connect.SmartConnect( |
| host=host, |
| port=port, |
| user=username, |
| pwd=password, |
| sslContext=context, |
| ) |
| atexit.register(connect.Disconnect, self.connection) |
| else: |
| raise LibcloudError("Cannot connect to vSphere", driver=self) |
| |
| def list_locations(self, ex_show_hosts_in_drs=True): |
| """ |
| Lists locations |
| """ |
| content = self.connection.RetrieveContent() |
| |
| potential_locations = [ |
| dc |
| for dc in content.viewManager.CreateContainerView( |
| content.rootFolder, |
| [vim.ClusterComputeResource, vim.HostSystem], |
| recursive=True, |
| ).view |
| ] |
| |
| # Add hosts and clusters with DRS enabled |
| locations = [] |
| hosts_all = [] |
| clusters = [] |
| for location in potential_locations: |
| if isinstance(location, vim.HostSystem): |
| hosts_all.append(location) |
| elif isinstance(location, vim.ClusterComputeResource): |
| if location.configuration.drsConfig.enabled: |
| clusters.append(location) |
| if ex_show_hosts_in_drs: |
| hosts = hosts_all |
| else: |
| hosts_filter = [host for cluster in clusters for host in cluster.host] |
| hosts = [host for host in hosts_all if host not in hosts_filter] |
| |
| for cluster in clusters: |
| locations.append(self._to_location(cluster)) |
| for host in hosts: |
| locations.append(self._to_location(host)) |
| return locations |
| |
| def _to_location(self, data): |
| try: |
| if isinstance(data, vim.HostSystem): |
| extra = { |
| "type": "host", |
| "state": data.runtime.connectionState, |
| "hypervisor": data.config.product.fullName, |
| "vendor": data.hardware.systemInfo.vendor, |
| "model": data.hardware.systemInfo.model, |
| "ram": data.hardware.memorySize, |
| "cpu": { |
| "packages": data.hardware.cpuInfo.numCpuPackages, |
| "cores": data.hardware.cpuInfo.numCpuCores, |
| "threads": data.hardware.cpuInfo.numCpuThreads, |
| }, |
| "uptime": data.summary.quickStats.uptime, |
| "parent": str(data.parent), |
| } |
| elif isinstance(data, vim.ClusterComputeResource): |
| extra = { |
| "type": "cluster", |
| "overallStatus": data.overallStatus, |
| "drs": data.configuration.drsConfig.enabled, |
| "hosts": [host.name for host in data.host], |
| "parent": str(data.parent), |
| } |
| except AttributeError as exc: |
| logger.error("Cannot convert location {}: {!r}".format(data.name, exc)) |
| extra = {} |
| return NodeLocation(id=data.name, name=data.name, country=None, extra=extra, driver=self) |
| |
| def ex_list_networks(self): |
| """ |
| List networks |
| """ |
| content = self.connection.RetrieveContent() |
| networks = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.Network], recursive=True |
| ).view |
| |
| return [self._to_network(network) for network in networks] |
| |
| def _to_network(self, data): |
| summary = data.summary |
| extra = { |
| "hosts": [h.name for h in data.host], |
| "ip_pool_name": summary.ipPoolName, |
| "ip_pool_id": summary.ipPoolId, |
| "accessible": summary.accessible, |
| } |
| return VSphereNetwork(id=data.name, name=data.name, extra=extra) |
| |
| def list_sizes(self): |
| """ |
| Returns sizes |
| """ |
| return [] |
| |
| def list_images(self, location=None, folder_ids=None): |
| """ |
| Lists VM templates as images. |
| If folder is given then it will list images contained |
| in that folder only. |
| """ |
| |
| images = [] |
| if folder_ids: |
| vms = [] |
| for folder_id in folder_ids: |
| folder_object = self._get_item_by_moid("Folder", folder_id) |
| vms.extend(folder_object.childEntity) |
| else: |
| content = self.connection.RetrieveContent() |
| vms = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.VirtualMachine], recursive=True |
| ).view |
| |
| for vm in vms: |
| if vm.config and vm.config.template: |
| images.append(self._to_image(vm)) |
| |
| return images |
| |
| def _to_image(self, data): |
| summary = data.summary |
| name = summary.config.name |
| uuid = summary.config.instanceUuid |
| memory = summary.config.memorySizeMB |
| cpus = summary.config.numCpu |
| operating_system = summary.config.guestFullName |
| os_type = "unix" |
| if "Microsoft" in str(operating_system): |
| os_type = "windows" |
| annotation = summary.config.annotation |
| extra = { |
| "path": summary.config.vmPathName, |
| "operating_system": operating_system, |
| "os_type": os_type, |
| "memory_MB": memory, |
| "cpus": cpus, |
| "overallStatus": str(summary.overallStatus), |
| "metadata": {}, |
| "type": "template_6_5", |
| "disk_size": int(summary.storage.committed) // (1024**3), |
| "datastore": data.datastore[0].info.name, |
| } |
| |
| boot_time = summary.runtime.bootTime |
| if boot_time: |
| extra["boot_time"] = boot_time.isoformat() |
| if annotation: |
| extra["annotation"] = annotation |
| |
| for custom_field in data.customValue: |
| key_id = custom_field.key |
| key = self.find_custom_field_key(key_id) |
| extra["metadata"][key] = custom_field.value |
| |
| return NodeImage(id=uuid, name=name, driver=self, extra=extra) |
| |
| def _collect_properties(self, content, view_ref, obj_type, path_set=None, include_mors=False): |
| """ |
| Collect properties for managed objects from a view ref |
| Check the vSphere API documentation for example on retrieving |
| object properties: |
| - http://goo.gl/erbFDz |
| Args: |
| content (ServiceInstance): ServiceInstance content |
| view_ref (pyVmomi.vim.view.*): Starting point of inventory |
| navigation |
| obj_type (pyVmomi.vim.*): Type of managed object |
| path_set (list): List of properties to retrieve |
| include_mors (bool): If True include the managed objects |
| refs in the result |
| Returns: |
| A list of properties for the managed objects |
| """ |
| collector = content.propertyCollector |
| |
| # Create object specification to define the starting point of |
| # inventory navigation |
| obj_spec = vmodl.query.PropertyCollector.ObjectSpec() |
| obj_spec.obj = view_ref |
| obj_spec.skip = True |
| |
| # Create a traversal specification to identify the path for collection |
| traversal_spec = vmodl.query.PropertyCollector.TraversalSpec() |
| traversal_spec.name = "traverseEntities" |
| traversal_spec.path = "view" |
| traversal_spec.skip = False |
| traversal_spec.type = view_ref.__class__ |
| obj_spec.selectSet = [traversal_spec] |
| |
| # Identify the properties to the retrieved |
| property_spec = vmodl.query.PropertyCollector.PropertySpec() |
| property_spec.type = obj_type |
| |
| if not path_set: |
| property_spec.all = True |
| |
| property_spec.pathSet = path_set |
| |
| # Add the object and property specification to the |
| # property filter specification |
| filter_spec = vmodl.query.PropertyCollector.FilterSpec() |
| filter_spec.objectSet = [obj_spec] |
| filter_spec.propSet = [property_spec] |
| |
| # Retrieve properties |
| props = collector.RetrieveContents([filter_spec]) |
| |
| data = [] |
| for obj in props: |
| properties = {} |
| for prop in obj.propSet: |
| properties[prop.name] = prop.val |
| |
| if include_mors: |
| properties["obj"] = obj.obj |
| |
| data.append(properties) |
| return data |
| |
| def list_nodes(self, enhance=True, max_properties=20): |
| """ |
| List nodes, excluding templates |
| """ |
| |
| vm_properties = [ |
| "config.template", |
| "summary.config.name", |
| "summary.config.vmPathName", |
| "summary.config.memorySizeMB", |
| "summary.config.numCpu", |
| "summary.storage.committed", |
| "summary.config.guestFullName", |
| "summary.runtime.host", |
| "summary.config.instanceUuid", |
| "summary.config.annotation", |
| "summary.runtime.powerState", |
| "summary.runtime.bootTime", |
| "summary.guest.ipAddress", |
| "summary.overallStatus", |
| "customValue", |
| "snapshot", |
| ] |
| content = self.connection.RetrieveContent() |
| view = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.VirtualMachine], True |
| ) |
| i = 0 |
| vm_dict = {} |
| while i < len(vm_properties): |
| vm_list = self._collect_properties( |
| content, |
| view, |
| vim.VirtualMachine, |
| path_set=vm_properties[i : i + max_properties], |
| include_mors=True, |
| ) |
| i += max_properties |
| for vm in vm_list: |
| if not vm_dict.get(vm["obj"]): |
| vm_dict[vm["obj"]] = vm |
| else: |
| vm_dict[vm["obj"]].update(vm) |
| |
| vm_list = [vm_dict[k] for k in vm_dict] |
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
| nodes = loop.run_until_complete(self._to_nodes(vm_list)) |
| if enhance: |
| nodes = self._enhance_metadata(nodes, content) |
| |
| return nodes |
| |
| def list_nodes_recursive(self, enhance=True): |
| """ |
| Lists nodes, excluding templates |
| """ |
| nodes = [] |
| content = self.connection.RetrieveContent() |
| children = content.rootFolder.childEntity |
| # this will be needed for custom VM metadata |
| if content.customFieldsManager: |
| self.custom_fields = content.customFieldsManager.field |
| else: |
| self.custom_fields = [] |
| for child in children: |
| if hasattr(child, "vmFolder"): |
| datacenter = child |
| vm_folder = datacenter.vmFolder |
| vm_list = vm_folder.childEntity |
| nodes.extend(self._to_nodes_recursive(vm_list)) |
| |
| if enhance: |
| nodes = self._enhance_metadata(nodes, content) |
| |
| return nodes |
| |
| def _enhance_metadata(self, nodes, content): |
| nodemap = {} |
| for node in nodes: |
| node.extra["vSphere version"] = content.about.version |
| nodemap[node.id] = node |
| |
| # Get VM deployment events to extract creation dates & images |
| filter_spec = vim.event.EventFilterSpec(eventTypeId=["VmBeingDeployedEvent"]) |
| deploy_events = content.eventManager.QueryEvent(filter_spec) |
| for event in deploy_events: |
| try: |
| uuid = event.vm.vm.config.instanceUuid |
| except Exception: |
| continue |
| if uuid in nodemap: |
| node = nodemap[uuid] |
| try: # Get source template as image |
| source_template_vm = event.srcTemplate.vm |
| image_id = source_template_vm.config.instanceUuid |
| node.extra["image_id"] = image_id |
| except Exception: |
| logger.error("Cannot get instanceUuid " "from source template") |
| try: # Get creation date |
| node.created_at = event.createdTime |
| except AttributeError: |
| logger.error("Cannot get creation date from VM " "deploy event") |
| |
| return nodes |
| |
| async def _to_nodes(self, vm_list): |
| vms = [] |
| for vm in vm_list: |
| if vm.get("config.template"): |
| continue # Do not include templates in node list |
| vms.append(vm) |
| loop = asyncio.get_event_loop() |
| vms = [loop.run_in_executor(None, self._to_node, vms[i]) for i in range(len(vms))] |
| return await asyncio.gather(*vms) |
| |
| def _to_nodes_recursive(self, vm_list): |
| nodes = [] |
| for virtual_machine in vm_list: |
| if hasattr(virtual_machine, "childEntity"): |
| # If this is a group it will have children. |
| # If it does, recurse into them and then return |
| nodes.extend(self._to_nodes_recursive(virtual_machine.childEntity)) |
| elif isinstance(virtual_machine, vim.VirtualApp): |
| # If this is a vApp, it likely contains child VMs |
| # (vApps can nest vApps, but it is hardly |
| # a common usecase, so ignore that) |
| nodes.extend(self._to_nodes_recursive(virtual_machine.vm)) |
| else: |
| if not hasattr(virtual_machine, "config") or ( |
| virtual_machine.config and virtual_machine.config.template |
| ): |
| continue # Do not include templates in node list |
| nodes.append(self._to_node_recursive(virtual_machine)) |
| return nodes |
| |
| def _to_node(self, vm): |
| name = vm.get("summary.config.name") |
| path = vm.get("summary.config.vmPathName") |
| memory = vm.get("summary.config.memorySizeMB") |
| cpus = vm.get("summary.config.numCpu") |
| disk = vm.get("summary.storage.committed", 0) // (1024**3) |
| id_to_hash = str(memory) + str(cpus) + str(disk) |
| size_id = hashlib.md5(id_to_hash.encode("utf-8")).hexdigest() |
| size_name = name + "-size" |
| size_extra = {"cpus": cpus} |
| driver = self |
| size = NodeSize( |
| id=size_id, |
| name=size_name, |
| ram=memory, |
| disk=disk, |
| bandwidth=0, |
| price=0, |
| driver=driver, |
| extra=size_extra, |
| ) |
| operating_system = vm.get("summary.config.guestFullName") |
| host = vm.get("summary.runtime.host") |
| |
| os_type = "unix" |
| if "Microsoft" in str(operating_system): |
| os_type = "windows" |
| uuid = vm.get("summary.config.instanceUuid") or ( |
| vm.get("obj").config and vm.get("obj").config.instanceUuid |
| ) |
| if not uuid: |
| logger.error("No uuid for vm: {}".format(vm)) |
| annotation = vm.get("summary.config.annotation") |
| state = vm.get("summary.runtime.powerState") |
| status = self.NODE_STATE_MAP.get(state, NodeState.UNKNOWN) |
| boot_time = vm.get("summary.runtime.bootTime") |
| |
| ip_addresses = [] |
| if vm.get("summary.guest.ipAddress"): |
| ip_addresses.append(vm.get("summary.guest.ipAddress")) |
| |
| overall_status = str(vm.get("summary.overallStatus")) |
| public_ips = [] |
| private_ips = [] |
| |
| extra = { |
| "path": path, |
| "operating_system": operating_system, |
| "os_type": os_type, |
| "memory_MB": memory, |
| "cpus": cpus, |
| "overall_status": overall_status, |
| "metadata": {}, |
| "snapshots": [], |
| } |
| |
| if disk: |
| extra["disk"] = disk |
| |
| if host: |
| extra["host"] = host.name |
| parent = host.parent |
| while parent: |
| if isinstance(parent, vim.ClusterComputeResource): |
| extra["cluster"] = parent.name |
| break |
| parent = parent.parent |
| |
| if boot_time: |
| extra["boot_time"] = boot_time.isoformat() |
| if annotation: |
| extra["annotation"] = annotation |
| |
| for ip_address in ip_addresses: |
| try: |
| if is_public_subnet(ip_address): |
| public_ips.append(ip_address) |
| else: |
| private_ips.append(ip_address) |
| except Exception: |
| # IPV6 not supported |
| pass |
| if vm.get("snapshot"): |
| extra["snapshots"] = format_snapshots( |
| recurse_snapshots(vm.get("snapshot").rootSnapshotList) |
| ) |
| |
| for custom_field in vm.get("customValue", []): |
| key_id = custom_field.key |
| key = self.find_custom_field_key(key_id) |
| extra["metadata"][key] = custom_field.value |
| |
| node = Node( |
| id=uuid, |
| name=name, |
| state=status, |
| size=size, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| extra=extra, |
| ) |
| node._uuid = uuid |
| return node |
| |
| def _to_node_recursive(self, virtual_machine): |
| summary = virtual_machine.summary |
| name = summary.config.name |
| path = summary.config.vmPathName |
| memory = summary.config.memorySizeMB |
| cpus = summary.config.numCpu |
| disk = "" |
| if summary.storage.committed: |
| disk = summary.storage.committed / (1024**3) |
| id_to_hash = str(memory) + str(cpus) + str(disk) |
| size_id = hashlib.md5(id_to_hash.encode("utf-8")).hexdigest() |
| size_name = name + "-size" |
| size_extra = {"cpus": cpus} |
| driver = self |
| size = NodeSize( |
| id=size_id, |
| name=size_name, |
| ram=memory, |
| disk=disk, |
| bandwidth=0, |
| price=0, |
| driver=driver, |
| extra=size_extra, |
| ) |
| operating_system = summary.config.guestFullName |
| host = summary.runtime.host |
| |
| # mist.io needs this metadata |
| os_type = "unix" |
| if "Microsoft" in str(operating_system): |
| os_type = "windows" |
| uuid = summary.config.instanceUuid |
| annotation = summary.config.annotation |
| state = summary.runtime.powerState |
| status = self.NODE_STATE_MAP.get(state, NodeState.UNKNOWN) |
| boot_time = summary.runtime.bootTime |
| ip_addresses = [] |
| if summary.guest is not None: |
| ip_addresses.append(summary.guest.ipAddress) |
| |
| overall_status = str(summary.overallStatus) |
| public_ips = [] |
| private_ips = [] |
| |
| extra = { |
| "path": path, |
| "operating_system": operating_system, |
| "os_type": os_type, |
| "memory_MB": memory, |
| "cpus": cpus, |
| "overallStatus": overall_status, |
| "metadata": {}, |
| "snapshots": [], |
| } |
| |
| if disk: |
| extra["disk"] = disk |
| |
| if host: |
| extra["host"] = host.name |
| parent = host.parent |
| while parent: |
| if isinstance(parent, vim.ClusterComputeResource): |
| extra["cluster"] = parent.name |
| break |
| parent = parent.parent |
| if boot_time: |
| extra["boot_time"] = boot_time.isoformat() |
| if annotation: |
| extra["annotation"] = annotation |
| |
| for ip_address in ip_addresses: |
| try: |
| if is_public_subnet(ip_address): |
| public_ips.append(ip_address) |
| else: |
| private_ips.append(ip_address) |
| except Exception: |
| # IPV6 not supported |
| pass |
| if virtual_machine.snapshot: |
| snapshots = [ |
| { |
| "id": s.id, |
| "name": s.name, |
| "description": s.description, |
| "created": s.createTime.strftime("%Y-%m-%d %H:%M"), |
| "state": s.state, |
| } |
| for s in virtual_machine.snapshot.rootSnapshotList |
| ] |
| extra["snapshots"] = snapshots |
| |
| for custom_field in virtual_machine.customValue: |
| key_id = custom_field.key |
| key = self.find_custom_field_key(key_id) |
| extra["metadata"][key] = custom_field.value |
| |
| node = Node( |
| id=uuid, |
| name=name, |
| state=status, |
| size=size, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| extra=extra, |
| ) |
| node._uuid = uuid |
| return node |
| |
| def reboot_node(self, node): |
| """ """ |
| vm = self.find_by_uuid(node.id) |
| return self.wait_for_task(vm.RebootGuest()) |
| |
| def destroy_node(self, node): |
| """ """ |
| vm = self.find_by_uuid(node.id) |
| if node.state != NodeState.STOPPED: |
| self.stop_node(node) |
| return self.wait_for_task(vm.Destroy()) |
| |
| def stop_node(self, node): |
| """ """ |
| vm = self.find_by_uuid(node.id) |
| return self.wait_for_task(vm.PowerOff()) |
| |
| def start_node(self, node): |
| """ """ |
| vm = self.find_by_uuid(node.id) |
| return self.wait_for_task(vm.PowerOn()) |
| |
| def ex_list_snapshots(self, node): |
| """ |
| List node snapshots |
| """ |
| vm = self.find_by_uuid(node.id) |
| if not vm.snapshot: |
| return [] |
| return format_snapshots(recurse_snapshots(vm.snapshot.rootSnapshotList)) |
| |
| def ex_create_snapshot( |
| self, node, snapshot_name, description="", dump_memory=False, quiesce=False |
| ): |
| """ |
| Create node snapshot |
| """ |
| vm = self.find_by_uuid(node.id) |
| return WaitForTask(vm.CreateSnapshot(snapshot_name, description, dump_memory, quiesce)) |
| |
| def ex_remove_snapshot(self, node, snapshot_name=None, remove_children=True): |
| """ |
| Remove a snapshot from node. |
| If snapshot_name is not defined remove the last one. |
| """ |
| vm = self.find_by_uuid(node.id) |
| if not vm.snapshot: |
| raise LibcloudError( |
| "Remove snapshot failed. No snapshots for node %s" % node.name, |
| driver=self, |
| ) |
| snapshots = recurse_snapshots(vm.snapshot.rootSnapshotList) |
| if not snapshot_name: |
| snapshot = snapshots[-1].snapshot |
| else: |
| for s in snapshots: |
| if snapshot_name == s.name: |
| snapshot = s.snapshot |
| break |
| else: |
| raise LibcloudError("Snapshot `%s` not found" % snapshot_name, driver=self) |
| return self.wait_for_task(snapshot.RemoveSnapshot_Task(removeChildren=remove_children)) |
| |
| def ex_revert_to_snapshot(self, node, snapshot_name=None): |
| """ |
| Revert node to a specific snapshot. |
| If snapshot_name is not defined revert to the last one. |
| """ |
| vm = self.find_by_uuid(node.id) |
| if not vm.snapshot: |
| raise LibcloudError( |
| "Revert failed. No snapshots " "for node %s" % node.name, driver=self |
| ) |
| snapshots = recurse_snapshots(vm.snapshot.rootSnapshotList) |
| if not snapshot_name: |
| snapshot = snapshots[-1].snapshot |
| else: |
| for s in snapshots: |
| if snapshot_name == s.name: |
| snapshot = s.snapshot |
| break |
| else: |
| raise LibcloudError("Snapshot `%s` not found" % snapshot_name, driver=self) |
| return self.wait_for_task(snapshot.RevertToSnapshot_Task()) |
| |
| def _find_template_by_uuid(self, template_uuid): |
| # on version 5.5 and earlier search index won't return a VM |
| try: |
| template = self.find_by_uuid(template_uuid) |
| except LibcloudError: |
| content = self.connection.RetrieveContent() |
| vms = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.VirtualMachine], recursive=True |
| ).view |
| |
| for vm in vms: |
| if vm.config.instanceUuid == template_uuid: |
| template = vm |
| except Exception as exc: |
| raise LibcloudError("Error while searching for template: %s" % exc, driver=self) |
| if not template: |
| raise LibcloudError("Unable to locate VirtualMachine.", driver=self) |
| |
| return template |
| |
| def find_by_uuid(self, node_uuid): |
| """Searches VMs for a given uuid |
| returns pyVmomi.VmomiSupport.vim.VirtualMachine |
| """ |
| vm = self.connection.content.searchIndex.FindByUuid(None, node_uuid, True, True) |
| if not vm: |
| # perhaps it is a moid |
| vm = self._get_item_by_moid("VirtualMachine", node_uuid) |
| if not vm: |
| raise LibcloudError("Unable to locate VirtualMachine.", driver=self) |
| return vm |
| |
| def find_custom_field_key(self, key_id): |
| """Return custom field key name, provided it's id""" |
| if not hasattr(self, "custom_fields"): |
| content = self.connection.RetrieveContent() |
| if content.customFieldsManager: |
| self.custom_fields = content.customFieldsManager.field |
| else: |
| self.custom_fields = [] |
| for k in self.custom_fields: |
| if k.key == key_id: |
| return k.name |
| return None |
| |
| def get_obj(self, vimtype, name): |
| """ |
| Return an object by name, if name is None the |
| first found object is returned |
| """ |
| obj = None |
| content = self.connection.RetrieveContent() |
| container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True) |
| for c in container.view: |
| if name: |
| if c.name == name: |
| obj = c |
| break |
| else: |
| obj = c |
| break |
| return obj |
| |
| def wait_for_task(self, task, timeout=1800, interval=10): |
| """wait for a vCenter task to finish""" |
| start_time = time.time() |
| task_done = False |
| while not task_done: |
| if time.time() - start_time >= timeout: |
| raise LibcloudError( |
| "Timeout while waiting " "for import task Id %s" % task.info.id, |
| driver=self, |
| ) |
| if task.info.state == "success": |
| if task.info.result and str(task.info.result) != "success": |
| return task.info.result |
| return True |
| |
| if task.info.state == "error": |
| raise LibcloudError(task.info.error.msg, driver=self) |
| time.sleep(interval) |
| |
| def create_node( |
| self, |
| name, |
| image, |
| size, |
| location=None, |
| ex_cluster=None, |
| ex_network=None, |
| ex_datacenter=None, |
| ex_folder=None, |
| ex_resource_pool=None, |
| ex_datastore_cluster=None, |
| ex_datastore=None, |
| ): |
| """ |
| Creates and returns node. |
| |
| :keyword ex_network: Name of a "Network" to connect the VM to ", |
| :type ex_network: ``str`` |
| |
| """ |
| template = self._find_template_by_uuid(image.id) |
| if ex_cluster: |
| cluster_name = ex_cluster |
| else: |
| cluster_name = location.name |
| cluster = self.get_obj([vim.ClusterComputeResource], cluster_name) |
| if not cluster: # It is a host go with it |
| cluster = self.get_obj([vim.HostSystem], cluster_name) |
| |
| datacenter = None |
| if not ex_datacenter: # Get datacenter from cluster |
| parent = cluster.parent |
| while parent: |
| if isinstance(parent, vim.Datacenter): |
| datacenter = parent |
| break |
| parent = parent.parent |
| |
| if ex_datacenter or datacenter is None: |
| datacenter = self.get_obj([vim.Datacenter], ex_datacenter) |
| |
| if ex_folder: |
| folder = self.get_obj([vim.Folder], ex_folder) |
| if folder is None: |
| folder = self._get_item_by_moid("Folder", ex_folder) |
| else: |
| folder = datacenter.vmFolder |
| |
| if ex_resource_pool: |
| resource_pool = self.get_obj([vim.ResourcePool], ex_resource_pool) |
| else: |
| try: |
| resource_pool = cluster.resourcePool |
| except AttributeError: |
| resource_pool = cluster.parent.resourcePool |
| devices = [] |
| vmconf = vim.vm.ConfigSpec( |
| numCPUs=int(size.extra.get("cpu", 1)), |
| memoryMB=int(size.ram), |
| deviceChange=devices, |
| ) |
| datastore = None |
| pod = None |
| podsel = vim.storageDrs.PodSelectionSpec() |
| if ex_datastore_cluster: |
| pod = self.get_obj([vim.StoragePod], ex_datastore_cluster) |
| else: |
| content = self.connection.RetrieveContent() |
| pods = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.StoragePod], True |
| ).view |
| for pod in pods: |
| if cluster.name.lower() in pod.name: |
| break |
| podsel.storagePod = pod |
| storagespec = vim.storageDrs.StoragePlacementSpec() |
| storagespec.podSelectionSpec = podsel |
| storagespec.type = "create" |
| storagespec.folder = folder |
| storagespec.resourcePool = resource_pool |
| storagespec.configSpec = vmconf |
| |
| try: |
| content = self.connection.RetrieveContent() |
| rec = content.storageResourceManager.RecommendDatastores(storageSpec=storagespec) |
| rec_action = rec.recommendations[0].action[0] |
| real_datastore_name = rec_action.destination.name |
| except Exception: |
| real_datastore_name = template.datastore[0].info.name |
| |
| datastore = self.get_obj([vim.Datastore], real_datastore_name) |
| |
| if ex_datastore: |
| datastore = self.get_obj([vim.Datastore], ex_datastore) |
| if datastore is None: |
| datastore = self._get_item_by_moid("Datastore", ex_datastore) |
| elif not datastore: |
| datastore = self.get_obj([vim.Datastore], template.datastore[0].info.name) |
| add_network = True |
| if ex_network and len(template.network) > 0: |
| for nets in template.network: |
| if template in nets.vm: |
| add_network = False |
| |
| if ex_network and add_network: |
| nicspec = vim.vm.device.VirtualDeviceSpec() |
| nicspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add |
| nicspec.device = vim.vm.device.VirtualVmxnet3() |
| nicspec.device.wakeOnLanEnabled = True |
| nicspec.device.deviceInfo = vim.Description() |
| |
| portgroup = self.get_obj([vim.dvs.DistributedVirtualPortgroup], ex_network) |
| if portgroup: |
| dvs_port_connection = vim.dvs.PortConnection() |
| dvs_port_connection.portgroupKey = portgroup.key |
| dvs_port_connection.switchUuid = portgroup.config.distributedVirtualSwitch.uuid |
| nicspec.device.backing = ( |
| vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() |
| ) |
| nicspec.device.backing.port = dvs_port_connection |
| else: |
| nicspec.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() |
| nicspec.device.backing.network = self.get_obj([vim.Network], ex_network) |
| nicspec.device.backing.deviceName = ex_network |
| nicspec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() |
| nicspec.device.connectable.startConnected = True |
| nicspec.device.connectable.connected = True |
| nicspec.device.connectable.allowGuestControl = True |
| devices.append(nicspec) |
| |
| # new_disk_kb = int(size.disk) * 1024 * 1024 |
| # disk_spec = vim.vm.device.VirtualDeviceSpec() |
| # disk_spec.fileOperation = "create" |
| # disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add |
| # disk_spec.device = vim.vm.device.VirtualDisk() |
| # disk_spec.device.backing = \ |
| # vim.vm.device.VirtualDisk.FlatVer2BackingInfo() |
| # if size.extra.get('disk_type') == 'thin': |
| # disk_spec.device.backing.thinProvisioned = True |
| # disk_spec.device.backing.diskMode = 'persistent' |
| # disk_spec.device.capacityInKB = new_disk_kb |
| # disk_spec.device.controllerKey = controller.key |
| # devices.append(disk_spec) |
| |
| clonespec = vim.vm.CloneSpec(config=vmconf) |
| relospec = vim.vm.RelocateSpec() |
| relospec.datastore = datastore |
| relospec.pool = resource_pool |
| if location: |
| host = self.get_obj([vim.HostSystem], location.name) |
| if host: |
| relospec.host = host |
| clonespec.location = relospec |
| clonespec.powerOn = True |
| task = template.Clone(folder=folder, name=name, spec=clonespec) |
| |
| return self._to_node_recursive(self.wait_for_task(task)) |
| |
| def ex_connect_network(self, vm, network_name): |
| spec = vim.vm.ConfigSpec() |
| |
| # add Switch here |
| dev_changes = [] |
| network_spec = vim.vm.device.VirtualDeviceSpec() |
| # network_spec.fileOperation = "create" |
| network_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add |
| network_spec.device = vim.vm.device.VirtualVmxnet3() |
| |
| network_spec.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() |
| |
| network_spec.device.backing.useAutoDetect = False |
| network_spec.device.backing.network = self.get_obj([vim.Network], network_name) |
| |
| network_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() |
| network_spec.device.connectable.startConnected = True |
| network_spec.device.connectable.connected = True |
| network_spec.device.connectable.allowGuestControl = True |
| |
| dev_changes.append(network_spec) |
| |
| spec.deviceChange = dev_changes |
| output = vm.ReconfigVM_Task(spec=spec) |
| print(output.info) |
| |
| def _get_item_by_moid(self, type_, moid): |
| vm_obj = VmomiSupport.templateOf(type_)(moid, self.connection._stub) |
| return vm_obj |
| |
| def ex_list_folders(self): |
| content = self.connection.RetrieveContent() |
| folders_raw = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.Folder], True |
| ).view |
| folders = [] |
| for folder in folders_raw: |
| to_add = {"type": list(folder.childType)} |
| to_add["name"] = folder.name |
| to_add["id"] = folder._moId |
| folders.append(to_add) |
| return folders |
| |
| def ex_list_datastores(self): |
| content = self.connection.RetrieveContent() |
| datastores_raw = content.viewManager.CreateContainerView( |
| content.rootFolder, [vim.Datastore], True |
| ).view |
| datastores = [] |
| |
| for dstore in datastores_raw: |
| to_add = {"type": dstore.summary.type} |
| to_add["name"] = dstore.name |
| to_add["id"] = dstore._moId |
| to_add["free_space"] = int(dstore.summary.freeSpace) |
| to_add["capacity"] = int(dstore.summary.capacity) |
| datastores.append(to_add) |
| |
| return datastores |
| |
| def ex_open_console(self, vm_uuid): |
| vm = self.find_by_uuid(vm_uuid) |
| ticket = vm.AcquireTicket(ticketType="webmks") |
| return "wss://{}:{}/ticket/{}".format(ticket.host, ticket.port, ticket.ticket) |
| |
| def _get_version(self): |
| content = self.connection.RetrieveContent() |
| return content.about.version |
| |
| |
| class VSphereNetwork: |
| """ |
| Represents information about a VPC (Virtual Private Cloud) network |
| |
| Note: This class is VSphere specific. |
| """ |
| |
| def __init__(self, id, name, extra=None): |
| self.id = id |
| self.name = name |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return ("<VSphereNetwork: id=%s, name=%s") % (self.id, self.name) |
| |
| |
| # 6.7 |
| class VSphereResponse(JsonResponse): |
| def parse_error(self): |
| if self.body: |
| message = self.body |
| message += "-- code: {}".format(self.status) |
| return message |
| return self.body |
| |
| |
| class VSphereConnection(ConnectionKey): |
| responseCls = VSphereResponse |
| session_token = None |
| |
| def add_default_headers(self, headers): |
| """ |
| VSphere needs an initial connection to a specific API endpoint to |
| generate a session-token, which will be used for the purpose of |
| authenticating for the rest of the session. |
| """ |
| headers["Content-Type"] = "application/json" |
| headers["Accept"] = "application/json" |
| if self.session_token is None: |
| to_encode = "{}:{}".format(self.key, self.secret) |
| b64_user_pass = base64.b64encode(to_encode.encode()) |
| headers["Authorization"] = "Basic {}".format(b64_user_pass.decode()) |
| else: |
| headers["vmware-api-session-id"] = self.session_token |
| return headers |
| |
| |
| class VSphereException(Exception): |
| def __init__(self, code, message): |
| self.code = code |
| self.message = message |
| self.args = (code, message) |
| |
| def __str__(self): |
| return "{} {}".format(self.code, self.message) |
| |
| def __repr__(self): |
| return "VSphereException {} {}".format(self.code, self.message) |
| |
| |
| class VSphere_REST_NodeDriver(NodeDriver): |
| name = "VMware vSphere" |
| website = "http://www.vmware.com/products/vsphere/" |
| type = Provider.VSPHERE |
| connectionCls = VSphereConnection |
| session_token = None |
| |
| NODE_STATE_MAP = { |
| "powered_on": NodeState.RUNNING, |
| "powered_off": NodeState.STOPPED, |
| "suspended": NodeState.SUSPENDED, |
| } |
| |
| VALID_RESPONSE_CODES = [ |
| httplib.OK, |
| httplib.ACCEPTED, |
| httplib.CREATED, |
| httplib.NO_CONTENT, |
| ] |
| |
| def __init__(self, key, secret=None, secure=True, host=None, port=443, ca_cert=None): |
| |
| if not key or not secret: |
| raise InvalidCredsError("Please provide both username " "(key) and password (secret).") |
| super().__init__(key=key, secure=secure, host=host, port=port) |
| prefixes = ["http://", "https://"] |
| for prefix in prefixes: |
| if host.startswith(prefix): |
| host = host.lstrip(prefix) |
| if ca_cert: |
| self.connection.connection.ca_cert = ca_cert |
| else: |
| self.connection.connection.ca_cert = False |
| |
| self.connection.secret = secret |
| self.host = host |
| self.username = key |
| # getting session token |
| self._get_session_token() |
| self.driver_soap = None |
| |
| def _get_soap_driver(self): |
| if pyvmomi is None: |
| raise ImportError( |
| 'Missing "pyvmomi" dependency. ' |
| "You can install it " |
| "using pip - pip install pyvmomi" |
| ) |
| self.driver_soap = VSphereNodeDriver( |
| self.host, |
| self.username, |
| self.connection.secret, |
| ca_cert=self.connection.connection.ca_cert, |
| ) |
| |
| def _get_session_token(self): |
| uri = "/rest/com/vmware/cis/session" |
| try: |
| result = self.connection.request(uri, method="POST") |
| except Exception: |
| raise |
| self.session_token = result.object["value"] |
| self.connection.session_token = self.session_token |
| |
| def list_sizes(self): |
| return [] |
| |
| def list_nodes( |
| self, |
| ex_filter_power_states=None, |
| ex_filter_folders=None, |
| ex_filter_names=None, |
| ex_filter_hosts=None, |
| ex_filter_clusters=None, |
| ex_filter_vms=None, |
| ex_filter_datacenters=None, |
| ex_filter_resource_pools=None, |
| max_properties=20, |
| ): |
| """ |
| The ex parameters are search options and must be an array of strings, |
| even ex_filter_power_states which can have at most two items but makes |
| sense to keep only one ("POWERED_ON" or "POWERED_OFF") |
| Keep in mind that this method will return up to 1000 nodes so if your |
| network has more, do use the provided filters and call it multiple |
| times. |
| """ |
| |
| req = "/rest/vcenter/vm" |
| kwargs = { |
| "filter.power_states": ex_filter_power_states, |
| "filter.folders": ex_filter_folders, |
| "filter.names": ex_filter_names, |
| "filter.hosts": ex_filter_hosts, |
| "filter.clusters": ex_filter_clusters, |
| "filter.vms": ex_filter_vms, |
| "filter.datacenters": ex_filter_datacenters, |
| "filter.resource_pools": ex_filter_resource_pools, |
| } |
| params = {} |
| for param, value in kwargs.items(): |
| if value: |
| params[param] = value |
| result = self._request(req, params=params).object["value"] |
| vm_ids = [[item["vm"]] for item in result] |
| vms = [] |
| interfaces = self._list_interfaces() |
| for vm_id in vm_ids: |
| vms.append(self._to_node(vm_id, interfaces)) |
| return vms |
| |
| def async_list_nodes(self): |
| """ |
| In this case filtering is not possible. |
| Use this method when the cloud has |
| a lot of vms and you want to return them all. |
| """ |
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
| result = loop.run_until_complete(self._get_all_vms()) |
| vm_ids = [(item["vm"], item["host"]) for item in result] |
| interfaces = self._list_interfaces() |
| return loop.run_until_complete(self._list_nodes_async(vm_ids, interfaces)) |
| |
| async def _list_nodes_async(self, vm_ids, interfaces): |
| loop = asyncio.get_event_loop() |
| vms = [ |
| loop.run_in_executor(None, self._to_node, vm_ids[i], interfaces) |
| for i in range(len(vm_ids)) |
| ] |
| |
| return await asyncio.gather(*vms) |
| |
| async def _get_all_vms(self): |
| """ |
| 6.7 doesn't offer any pagination, if we get 1000 vms we will try |
| this roundabout way: First get all the datacenters, for each |
| datacenter get the hosts and for each host the vms it has. |
| This assumes that datacenters, hosts per datacenter and vms per |
| host don't exceed 1000. |
| """ |
| datacenters = self.ex_list_datacenters() |
| loop = asyncio.get_event_loop() |
| hosts_futures = [ |
| loop.run_in_executor( |
| None, |
| functools.partial(self.ex_list_hosts, ex_filter_datacenters=datacenter["id"]), |
| ) |
| for datacenter in datacenters |
| ] |
| hosts = await asyncio.gather(*hosts_futures) |
| |
| vm_resp_futures = [ |
| loop.run_in_executor(None, functools.partial(self._get_vms_with_host, host)) |
| for host in itertools.chain(*hosts) |
| ] |
| |
| vm_resp = await asyncio.gather(*vm_resp_futures) |
| # return a flat list |
| return [item for vm_list in vm_resp for item in vm_list] |
| |
| def _get_vms_with_host(self, host): |
| req = "/rest/vcenter/vm" |
| host_id = host["host"] |
| response = self._request(req, params={"filter.hosts": host_id}) |
| vms = response.object["value"] |
| for vm in vms: |
| vm["host"] = host |
| return vms |
| |
| def list_locations(self, ex_show_hosts_in_drs=True): |
| """ |
| Location in the general sense means any resource that allows for node |
| creation. In vSphere's case that usually is a host but if a cluster |
| has rds enabled, a cluster can be assigned to create the VM, thus the |
| clusters with rds enabled will be added to locations. |
| |
| :param ex_show_hosts_in_drs: A DRS cluster schedules automatically |
| on which host should be placed thus it |
| may not be desired to show the hosts |
| in a DRS enabled cluster. Set False to |
| not show these hosts. |
| :type ex_show_hosts_in_drs: `boolean` |
| """ |
| clusters = self.ex_list_clusters() |
| hosts_all = self.ex_list_hosts() |
| hosts = [] |
| if ex_show_hosts_in_drs: |
| hosts = hosts_all |
| else: |
| cluster_filter = [cluster["cluster"] for cluster in clusters] |
| filter_hosts = self.ex_list_hosts(ex_filter_clusters=cluster_filter) |
| hosts = [host for host in hosts_all if host not in filter_hosts] |
| driver = self.connection.driver |
| locations = [] |
| for cluster in clusters: |
| if cluster["drs_enabled"]: |
| extra = {"type": "cluster", "drs": True, "ha": cluster["ha_enabled"]} |
| locations.append( |
| NodeLocation( |
| id=cluster["cluster"], |
| name=cluster["name"], |
| country="", |
| driver=driver, |
| extra=extra, |
| ) |
| ) |
| for host in hosts: |
| extra = { |
| "type": "host", |
| "status": host["connection_state"], |
| "state": host["power_state"], |
| } |
| |
| locations.append( |
| NodeLocation( |
| id=host["host"], |
| name=host["name"], |
| country="", |
| driver=driver, |
| extra=extra, |
| ) |
| ) |
| return locations |
| |
| def stop_node(self, node): |
| if node.state == NodeState.STOPPED: |
| return True |
| |
| method = "POST" |
| req = "/rest/vcenter/vm/{}/power/stop".format(node.id) |
| |
| result = self._request(req, method=method) |
| return result.status in self.VALID_RESPONSE_CODES |
| |
| def start_node(self, node): |
| if isinstance(node, str): |
| node_id = node |
| else: |
| if node.state is NodeState.RUNNING: |
| return True |
| node_id = node.id |
| method = "POST" |
| req = "/rest/vcenter/vm/{}/power/start".format(node_id) |
| result = self._request(req, method=method) |
| return result.status in self.VALID_RESPONSE_CODES |
| |
| def reboot_node(self, node): |
| if node.state is not NodeState.RUNNING: |
| return False |
| |
| method = "POST" |
| req = "/rest/vcenter/vm/{}/power/reset".format(node.id) |
| result = self._request(req, method=method) |
| return result.status in self.VALID_RESPONSE_CODES |
| |
| def destroy_node(self, node): |
| # make sure the machine is stopped |
| if node.state is not NodeState.STOPPED: |
| self.stop_node(node) |
| # wait to make sure it stopped |
| # in the future this can be made asynchronously |
| # for i in range(6): |
| # if node.state is NodeState.STOPPED: |
| # break |
| # time.sleep(10) |
| req = "/rest/vcenter/vm/{}".format(node.id) |
| resp = self._request(req, method="DELETE") |
| return resp.status in self.VALID_RESPONSE_CODES |
| |
| def ex_suspend_node(self, node): |
| if node.state is not NodeState.RUNNING: |
| return False |
| |
| method = "POST" |
| req = "/rest/vcenter/vm/{}/power/suspend".format(node.id) |
| result = self._request(req, method=method) |
| return result.status in self.VALID_RESPONSE_CODES |
| |
| def _list_interfaces(self): |
| request = "/rest/appliance/networking/interfaces" |
| response = self._request(request).object["value"] |
| interfaces = [ |
| { |
| "name": interface["name"], |
| "mac": interface["mac"], |
| "status": interface["status"], |
| "ip": interface["ipv4"]["address"], |
| } |
| for interface in response |
| ] |
| return interfaces |
| |
| def _to_node(self, vm_id_host, interfaces): |
| """ |
| id, name, state, public_ips, private_ips, |
| driver, size=None, image=None, extra=None, created_at=None) |
| """ |
| vm_id = vm_id_host[0] |
| req = "/rest/vcenter/vm/" + vm_id |
| vm = self._request(req).object["value"] |
| name = vm["name"] |
| state = self.NODE_STATE_MAP[vm["power_state"].lower()] |
| |
| # IP's |
| private_ips = [] |
| nic_macs = set() |
| for nic in vm["nics"]: |
| nic_macs.add(nic["value"]["mac_address"]) |
| for interface in interfaces: |
| if interface["mac"] in nic_macs: |
| private_ips.append(interface["ip"]) |
| nic_macs.remove(interface["mac"]) |
| if len(nic_macs) == 0: |
| break |
| public_ips = [] # should default_getaway be the public? |
| |
| driver = self.connection.driver |
| |
| # size |
| total_size = 0 |
| gb_converter = 1024**3 |
| for disk in vm["disks"]: |
| total_size += int(int(disk["value"]["capacity"] / gb_converter)) |
| ram = int(vm["memory"]["size_MiB"]) |
| cpus = int(vm["cpu"]["count"]) |
| id_to_hash = str(ram) + str(cpus) + str(total_size) |
| size_id = hashlib.md5(id_to_hash.encode("utf-8")).hexdigest() |
| size_name = name + "-size" |
| size_extra = {"cpus": cpus} |
| size = NodeSize( |
| id=size_id, |
| name=size_name, |
| ram=ram, |
| disk=total_size, |
| bandwidth=0, |
| price=0, |
| driver=driver, |
| extra=size_extra, |
| ) |
| |
| # image |
| image_name = vm["guest_OS"] |
| image_id = image_name + "_id" |
| image_extra = {"type": "guest_OS"} |
| image = NodeImage(id=image_id, name=image_name, driver=driver, extra=image_extra) |
| extra = {"snapshots": []} |
| if len(vm_id_host) > 1: |
| extra["host"] = vm_id_host[1].get("name", "") |
| return Node( |
| id=vm_id, |
| name=name, |
| state=state, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=driver, |
| size=size, |
| image=image, |
| extra=extra, |
| ) |
| |
| def ex_list_hosts( |
| self, |
| ex_filter_folders=None, |
| ex_filter_standalone=None, |
| ex_filter_hosts=None, |
| ex_filter_clusters=None, |
| ex_filter_names=None, |
| ex_filter_datacenters=None, |
| ex_filter_connection_states=None, |
| ): |
| |
| kwargs = { |
| "filter.folders": ex_filter_folders, |
| "filter.names": ex_filter_names, |
| "filter.hosts": ex_filter_hosts, |
| "filter.clusters": ex_filter_clusters, |
| "filter.standalone": ex_filter_standalone, |
| "filter.datacenters": ex_filter_datacenters, |
| "filter.connection_states": ex_filter_connection_states, |
| } |
| |
| params = {} |
| for param, value in kwargs.items(): |
| if value: |
| params[param] = value |
| req = "/rest/vcenter/host" |
| result = self._request(req, params=params).object["value"] |
| return result |
| |
| def ex_list_clusters( |
| self, |
| ex_filter_folders=None, |
| ex_filter_names=None, |
| ex_filter_datacenters=None, |
| ex_filter_clusters=None, |
| ): |
| kwargs = { |
| "filter.folders": ex_filter_folders, |
| "filter.names": ex_filter_names, |
| "filter.datacenters": ex_filter_datacenters, |
| "filter.clusters": ex_filter_clusters, |
| } |
| params = {} |
| for param, value in kwargs.items(): |
| if value: |
| params[param] = value |
| req = "/rest/vcenter/cluster" |
| result = self._request(req, params=params).object["value"] |
| return result |
| |
| def ex_list_datacenters( |
| self, ex_filter_folders=None, ex_filter_names=None, ex_filter_datacenters=None |
| ): |
| req = "/rest/vcenter/datacenter" |
| kwargs = { |
| "filter.folders": ex_filter_folders, |
| "filter.names": ex_filter_names, |
| "filter.datacenters": ex_filter_datacenters, |
| } |
| params = {} |
| for param, value in kwargs.items(): |
| if value: |
| params[param] = value |
| result = self._request(req, params=params) |
| to_return = [ |
| {"name": item["name"], "id": item["datacenter"]} for item in result.object["value"] |
| ] |
| return to_return |
| |
| def ex_list_content_libraries(self): |
| req = "/rest/com/vmware/content/library" |
| try: |
| result = self._request(req).object |
| return result["value"] |
| except BaseHTTPError: |
| return [] |
| |
| def ex_list_content_library_items(self, library_id): |
| req = "/rest/com/vmware/content/library/item" |
| params = {"library_id": library_id} |
| try: |
| result = self._request(req, params=params).object |
| return result["value"] |
| except BaseHTTPError: |
| logger.error( |
| "Library was cannot be accesed, " " most probably the VCenter service " "is stopped" |
| ) |
| return [] |
| |
| def ex_list_folders(self): |
| req = "/rest/vcenter/folder" |
| response = self._request(req).object |
| folders = response["value"] |
| for folder in folders: |
| folder["id"] = folder["folder"] |
| return folders |
| |
| def ex_list_datastores( |
| self, |
| ex_filter_folders=None, |
| ex_filter_names=None, |
| ex_filter_datacenters=None, |
| ex_filter_types=None, |
| ex_filter_datastores=None, |
| ): |
| req = "/rest/vcenter/datastore" |
| kwargs = { |
| "filter.folders": ex_filter_folders, |
| "filter.names": ex_filter_names, |
| "filter.datacenters": ex_filter_datacenters, |
| "filter.types": ex_filter_types, |
| "filter.datastores": ex_filter_datastores, |
| } |
| params = {} |
| for param, value in kwargs.items(): |
| if value: |
| params[param] = value |
| result = self._request(req, params=params).object["value"] |
| for datastore in result: |
| datastore["id"] = datastore["datastore"] |
| return result |
| |
| def ex_update_memory(self, node, ram): |
| """ |
| :param ram: The ammount of ram in MB. |
| :type ram: `str` or `int` |
| """ |
| if isinstance(node, str): |
| node_id = node |
| else: |
| node_id = node.id |
| request = "/rest/vcenter/vm/{}/hardware/memory".format(node_id) |
| ram = int(ram) |
| |
| body = {"spec": {"size_MiB": ram}} |
| response = self._request(request, method="PATCH", data=json.dumps(body)) |
| return response.status in self.VALID_RESPONSE_CODES |
| |
| def ex_update_cpu(self, node, cores): |
| """ |
| Assuming 1 Core per socket |
| :param cores: Integer or string indicating number of cores |
| :type cores: `int` or `str` |
| """ |
| if isinstance(node, str): |
| node_id = node |
| else: |
| node_id = node.id |
| request = "/rest/vcenter/vm/{}/hardware/cpu".format(node_id) |
| cores = int(cores) |
| body = {"spec": {"count": cores}} |
| response = self._request(request, method="PATCH", data=json.dumps(body)) |
| return response.status in self.VALID_RESPONSE_CODES |
| |
| def ex_update_capacity(self, node, capacity): |
| # Should be added when REST API supports it |
| pass |
| |
| def ex_add_nic(self, node, network): |
| """ |
| Creates a network adapater that will connect to the specified network |
| for the given node. Returns a boolean indicating success or not. |
| """ |
| if isinstance(node, str): |
| node_id = node |
| else: |
| node_id = node.id |
| spec = {} |
| spec["mac_type"] = "GENERATED" |
| spec["backing"] = {} |
| spec["backing"]["type"] = "STANDARD_PORTGROUP" |
| spec["backing"]["network"] = network |
| spec["start_connected"] = True |
| |
| data = json.dumps({"spec": spec}) |
| req = "/rest/vcenter/vm/{}/hardware/ethernet".format(node_id) |
| method = "POST" |
| resp = self._request(req, method=method, data=data) |
| return resp.status |
| |
| def _get_library_item(self, item_id): |
| req = "/rest/com/vmware/content/library/item/id:{}".format(item_id) |
| result = self._request(req).object |
| return result["value"] |
| |
| def _get_resource_pool(self, host_id=None, cluster_id=None, name=None): |
| if host_id: |
| pms = {"filter.hosts": host_id} |
| if cluster_id: |
| pms = {"filter.clusters": cluster_id} |
| if name: |
| pms = {"filter.names": name} |
| rp_request = "/rest/vcenter/resource-pool" |
| resource_pool = self._request(rp_request, params=pms).object |
| return resource_pool["value"][0]["resource_pool"] |
| |
| def _request(self, req, method="GET", params=None, data=None): |
| try: |
| result = self.connection.request(req, method=method, params=params, data=data) |
| except BaseHTTPError as exc: |
| if exc.code == 401: |
| self.connection.session_token = None |
| self._get_session_token() |
| result = self.connection.request(req, method=method, params=params, data=data) |
| else: |
| raise |
| except Exception: |
| raise |
| return result |
| |
| def list_images(self, **kwargs): |
| libraries = self.ex_list_content_libraries() |
| item_ids = [] |
| if libraries: |
| for library in libraries: |
| item_ids.extend(self.ex_list_content_library_items(library)) |
| items = [] |
| if item_ids: |
| for item_id in item_ids: |
| items.append(self._get_library_item(item_id)) |
| images = [] |
| names = set() |
| if items: |
| driver = self.connection.driver |
| for item in items: |
| names.add(item["name"]) |
| extra = {"type": item["type"]} |
| if item["type"] == "vm-template": |
| capacity = item["size"] // (1024**3) |
| extra["disk_size"] = capacity |
| images.append( |
| NodeImage(id=item["id"], name=item["name"], driver=driver, extra=extra) |
| ) |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| templates_in_hosts = self.driver_soap.list_images() |
| for template in templates_in_hosts: |
| if template.name not in names: |
| images += [template] |
| return images |
| |
| def ex_list_networks(self): |
| |
| request = "/rest/vcenter/network" |
| response = self._request(request).object["value"] |
| networks = [] |
| for network in response: |
| networks.append( |
| VSphereNetwork( |
| id=network["network"], |
| name=network["name"], |
| extra={"type": network["type"]}, |
| ) |
| ) |
| return networks |
| |
| def create_node( |
| self, |
| name, |
| image, |
| size=None, |
| location=None, |
| ex_datastore=None, |
| ex_disks=None, |
| ex_folder=None, |
| ex_network=None, |
| ex_turned_on=True, |
| ): |
| """ |
| Image can be either a vm template , a ovf template or just |
| the guest OS. |
| |
| ex_folder is necessary if the image is a vm-template, this method |
| will attempt to put the VM in a random folder and a warning about it |
| will be issued in case the value remains `None`. |
| """ |
| # image is in the host then need the 6.5 driver |
| if image.extra["type"] == "template_6_5": |
| kwargs = {} |
| kwargs["name"] = name |
| kwargs["image"] = image |
| kwargs["size"] = size |
| kwargs["ex_network"] = ex_network |
| kwargs["location"] = location |
| for dstore in self.ex_list_datastores(): |
| if dstore["id"] == ex_datastore: |
| kwargs["ex_datastore"] = dstore["name"] |
| break |
| kwargs["folder"] = ex_folder |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| result = self.driver_soap.create_node(**kwargs) |
| return result |
| |
| # post creation checks |
| create_nic = False |
| update_memory = False |
| update_cpu = False |
| create_disk = False |
| update_capacity = False |
| if image.extra["type"] == "guest_OS": |
| spec = {} |
| spec["guest_OS"] = image.name |
| spec["name"] = name |
| spec["placement"] = {} |
| if ex_folder is None: |
| warn = ( |
| "The API(6.7) requires the folder to be given, I will" |
| " place it into a random folder, after creation you " |
| "might find it convenient to move it into a better " |
| "folder." |
| ) |
| warnings.warn(warn) |
| folders = self.ex_list_folders() |
| for folder in folders: |
| if folder["type"] == "VIRTUAL_MACHINE": |
| ex_folder = folder["folder"] |
| if ex_folder is None: |
| msg = "No suitable folder vor VMs found, please create one" |
| raise ProviderError(msg, 404) |
| spec["placement"]["folder"] = ex_folder |
| if location.extra["type"] == "host": |
| spec["placement"]["host"] = location.id |
| elif location.extra["type"] == "cluster": |
| spec["placement"]["cluster"] = location.id |
| elif location.extra["type"] == "resource_pool": |
| spec["placement"]["resource_pool"] = location.id |
| spec["placement"]["datastore"] = ex_datastore |
| cpu = size.extra.get("cpu", 1) |
| spec["cpu"] = {"count": cpu} |
| spec["memory"] = {"size_MiB": size.ram} |
| if size.disk: |
| disk = {} |
| disk["new_vmdk"] = {} |
| disk["new_vmdk"]["capacity"] = size.disk * (1024**3) |
| spec["disks"] = [disk] |
| if ex_network: |
| nic = {} |
| nic["mac_type"] = "GENERATED" |
| nic["backing"] = {} |
| nic["backing"]["type"] = "STANDARD_PORTGROUP" |
| nic["backing"]["network"] = ex_network |
| nic["start_connected"] = True |
| spec["nics"] = [nic] |
| create_request = "/rest/vcenter/vm" |
| data = json.dumps({"spec": spec}) |
| |
| elif image.extra["type"] == "ovf": |
| ovf_request = ( |
| "/rest/com/vmware/vcenter/ovf/library-item" "/id:{}?~action=filter".format(image.id) |
| ) |
| spec = {} |
| spec["target"] = {} |
| if location.extra.get("type") == "resource-pool": |
| spec["target"]["resource_pool_id"] = location.id |
| elif location.extra.get("type") == "host": |
| resource_pool = self._get_resource_pool(host_id=location.id) |
| if not resource_pool: |
| msg = ( |
| "Could not find resource-pool for given location " |
| "(host). Please make sure the location is valid." |
| ) |
| raise VSphereException(code="504", message=msg) |
| spec["target"]["resource_pool_id"] = resource_pool |
| spec["target"]["host_id"] = location.id |
| elif location.extra.get("type") == "cluster": |
| resource_pool = self._get_resource_pool(cluster_id=location.id) |
| if not resource_pool: |
| msg = ( |
| "Could not find resource-pool for given location " |
| "(cluster). Please make sure the location " |
| "is valid." |
| ) |
| raise VSphereException(code="504", message=msg) |
| spec["target"]["resource_pool_id"] = resource_pool |
| ovf = self._request(ovf_request, method="POST", data=json.dumps(spec)).object["value"] |
| spec["deployment_spec"] = {} |
| spec["deployment_spec"]["name"] = name |
| # assuming that since you want to make a vm you don't need reminder |
| spec["deployment_spec"]["accept_all_EULA"] = True |
| # network |
| if ex_network and ovf["networks"]: |
| spec["deployment_spec"]["network_mappings"] = [ |
| {"key": ovf["networks"][0], "value": ex_network} |
| ] |
| elif not ovf["networks"] and ex_network: |
| create_nic = True |
| # storage |
| if ex_datastore: |
| spec["deployment_spec"]["storage_mappings"] = [] |
| store_map = {"type": "DATASTORE", "datastore_id": ex_datastore} |
| spec["deployment_spec"]["storage_mappings"].append(store_map) |
| if size and size.ram: |
| update_memory = True |
| if size and size.extra and size.extra.get("cpu"): |
| update_cpu = True |
| if size and size.disk: |
| # TODO Should update capacity but it is not possible with 6.7 |
| pass |
| if ex_disks: |
| create_disk = True |
| |
| create_request = ( |
| "/rest/com/vmware/vcenter/ovf/library-item" "/id:{}?~action=deploy".format(image.id) |
| ) |
| data = json.dumps( |
| {"target": spec["target"], "deployment_spec": spec["deployment_spec"]} |
| ) |
| |
| elif image.extra["type"] == "vm-template": |
| |
| tp_request = "/rest/vcenter/vm-template/library-items/" + image.id |
| template = self._request(tp_request).object["value"] |
| spec = {} |
| spec["name"] = name |
| |
| # storage |
| if ex_datastore: |
| spec["disk_storage"] = {} |
| spec["disk_storage"]["datastore"] = ex_datastore |
| |
| # location :: folder,resource group, datacenter, host |
| spec["placement"] = {} |
| if not ex_folder: |
| warn = ( |
| "The API(6.7) requires the folder to be given, I will" |
| " place it into a random folder, after creation you " |
| "might find it convenient to move it into a better " |
| "folder." |
| ) |
| warnings.warn(warn) |
| folders = self.ex_list_folders() |
| for folder in folders: |
| if folder["type"] == "VIRTUAL_MACHINE": |
| ex_folder = folder["folder"] |
| if ex_folder is None: |
| msg = "No suitable folder vor VMs found, please create one" |
| raise ProviderError(msg, 404) |
| spec["placement"]["folder"] = ex_folder |
| if location.extra["type"] == "host": |
| spec["placement"]["host"] = location.id |
| elif location.extra["type"] == "cluster": |
| spec["placement"]["cluster"] = location.id |
| # network changes the network to existing nics if |
| # there are no adapters |
| # in the template then we will make on in the vm |
| # after the creation finishes |
| # only one network atm?? |
| spec["hardware_customization"] = {} |
| if ex_network: |
| nics = template["nics"] |
| if len(nics) > 0: |
| nic = nics[0] |
| spec["hardware_customization"]["nics"] = [ |
| {"key": nic["key"], "value": {"network": ex_network}} |
| ] |
| else: |
| create_nic = True |
| spec["powered_on"] = False |
| # hardware |
| if size: |
| if size.ram: |
| spec["hardware_customization"]["memory_update"] = {"memory": int(size.ram)} |
| if size.extra.get("cpu"): |
| spec["hardware_customization"]["cpu_update"] = {"num_cpus": size.extra["cpu"]} |
| if size.disk: |
| if not len(template["disks"]) > 0: |
| create_disk = True |
| else: |
| capacity = size.disk * 1024 * 1024 * 1024 |
| dsk = template["disks"][0]["key"] |
| if template["disks"][0]["value"]["capacity"] < capacity: |
| update = {"capacity": capacity} |
| spec["hardware_customization"]["disks_to_update"] = [ |
| {"key": dsk, "value": update} |
| ] |
| |
| create_request = "/rest/vcenter/vm-template/library-items/" "{}/?action=deploy".format( |
| image.id |
| ) |
| data = json.dumps({"spec": spec}) |
| # deploy the node |
| result = self._request(create_request, method="POST", data=data) |
| # wait until the node is up and then add extra config |
| node_id = result.object["value"] |
| if image.extra["type"] == "ovf": |
| node_id = node_id["resource_id"]["id"] |
| |
| node = self.list_nodes(ex_filter_vms=node_id)[0] |
| if create_nic: |
| self.ex_add_nic(node, ex_network) |
| if update_memory: |
| self.ex_update_memory(node, size.ram) |
| if update_cpu: |
| self.ex_update_cpu(node, size.extra["cpu"]) |
| if create_disk: |
| pass # until volumes are added |
| if update_capacity: |
| pass # until API method is added |
| if ex_turned_on: |
| self.start_node(node) |
| return node |
| |
| # TODO As soon as snapshot support gets added to the REST api |
| # these methods should be rewritten with REST api calls |
| def ex_list_snapshots(self, node): |
| """ |
| List node snapshots |
| """ |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| return self.driver_soap.ex_list_snapshots(node) |
| |
| def ex_create_snapshot( |
| self, node, snapshot_name, description="", dump_memory=False, quiesce=False |
| ): |
| """ |
| Create node snapshot |
| """ |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| return self.driver_soap.ex_create_snapshot( |
| node, |
| snapshot_name, |
| description=description, |
| dump_memory=dump_memory, |
| quiesce=False, |
| ) |
| |
| def ex_remove_snapshot(self, node, snapshot_name=None, remove_children=True): |
| """ |
| Remove a snapshot from node. |
| If snapshot_name is not defined remove the last one. |
| """ |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| return self.driver_soap.ex_remove_snapshot( |
| node, snapshot_name=snapshot_name, remove_children=remove_children |
| ) |
| |
| def ex_revert_to_snapshot(self, node, snapshot_name=None): |
| """ |
| Revert node to a specific snapshot. |
| If snapshot_name is not defined revert to the last one. |
| """ |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| return self.driver_soap.ex_revert_to_snapshot(node, snapshot_name=snapshot_name) |
| |
| def ex_open_console(self, vm_id): |
| if self.driver_soap is None: |
| self._get_soap_driver() |
| return self.driver_soap.ex_open_console(vm_id) |