"""Support for Proxmox VE.""" from __future__ import annotations from datetime import timedelta import logging from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError from proxmoxer.core import ResourceException import requests.exceptions from requests.exceptions import ConnectTimeout, SSLError import voluptuous as vol from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, Platform, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) PLATFORMS = [Platform.BINARY_SENSOR] DOMAIN = "proxmoxve" PROXMOX_CLIENTS = "proxmox_clients" CONF_REALM = "realm" CONF_NODE = "node" CONF_NODES = "nodes" CONF_VMS = "vms" CONF_CONTAINERS = "containers" COORDINATORS = "coordinators" API_DATA = "api_data" DEFAULT_PORT = 8006 DEFAULT_REALM = "pam" DEFAULT_VERIFY_SSL = True TYPE_VM = 0 TYPE_CONTAINER = 1 UPDATE_INTERVAL = 60 _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.ensure_list, [ vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string, vol.Optional( CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL ): cv.boolean, vol.Required(CONF_NODES): vol.All( cv.ensure_list, [ vol.Schema( { vol.Required(CONF_NODE): cv.string, vol.Optional(CONF_VMS, default=[]): [ cv.positive_int ], vol.Optional(CONF_CONTAINERS, default=[]): [ cv.positive_int ], } ) ], ), } ) ], ) }, extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the platform.""" hass.data.setdefault(DOMAIN, {}) def build_client() -> ProxmoxAPI: """Build the Proxmox client connection.""" hass.data[PROXMOX_CLIENTS] = {} for entry in config[DOMAIN]: host = entry[CONF_HOST] port = entry[CONF_PORT] user = entry[CONF_USERNAME] realm = entry[CONF_REALM] password = entry[CONF_PASSWORD] verify_ssl = entry[CONF_VERIFY_SSL] hass.data[PROXMOX_CLIENTS][host] = None try: # Construct an API client with the given data for the given host proxmox_client = ProxmoxClient( host, port, user, realm, password, verify_ssl ) proxmox_client.build_client() except AuthenticationError: _LOGGER.warning( "Invalid credentials for proxmox instance %s:%d", host, port ) continue except SSLError: _LOGGER.error( "Unable to verify proxmox server SSL. " 'Try using "verify_ssl: false" for proxmox instance %s:%d', host, port, ) continue except ConnectTimeout: _LOGGER.warning("Connection to host %s timed out during setup", host) continue except requests.exceptions.ConnectionError: _LOGGER.warning("Host %s is not reachable", host) continue hass.data[PROXMOX_CLIENTS][host] = proxmox_client await hass.async_add_executor_job(build_client) coordinators: dict[str, dict[str, dict[int, DataUpdateCoordinator]]] = {} hass.data[DOMAIN][COORDINATORS] = coordinators # Create a coordinator for each vm/container for host_config in config[DOMAIN]: host_name = host_config["host"] coordinators[host_name] = {} proxmox_client = hass.data[PROXMOX_CLIENTS][host_name] # Skip invalid hosts if proxmox_client is None: continue proxmox = proxmox_client.get_api_client() for node_config in host_config["nodes"]: node_name = node_config["node"] node_coordinators = coordinators[host_name][node_name] = {} for vm_id in node_config["vms"]: coordinator = create_coordinator_container_vm( hass, proxmox, host_name, node_name, vm_id, TYPE_VM ) # Fetch initial data await coordinator.async_refresh() node_coordinators[vm_id] = coordinator for container_id in node_config["containers"]: coordinator = create_coordinator_container_vm( hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER ) # Fetch initial data await coordinator.async_refresh() node_coordinators[container_id] = coordinator for component in PLATFORMS: await hass.async_create_task( async_load_platform(hass, component, DOMAIN, {"config": config}, config) ) return True def create_coordinator_container_vm( hass, proxmox, host_name, node_name, vm_id, vm_type ): """Create and return a DataUpdateCoordinator for a vm/container.""" async def async_update_data(): """Call the api and handle the response.""" def poll_api(): """Call the api.""" vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) return vm_status vm_status = await hass.async_add_executor_job(poll_api) if vm_status is None: _LOGGER.warning( "Vm/Container %s unable to be found in node %s", vm_id, node_name ) return None return parse_api_container_vm(vm_status) return DataUpdateCoordinator( hass, _LOGGER, name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) def parse_api_container_vm(status): """Get the container or vm api data and return it formatted in a dictionary. It is implemented in this way to allow for more data to be added for sensors in the future. """ return {"status": status["status"], "name": status["name"]} def call_api_container_vm(proxmox, node_name, vm_id, machine_type): """Make proper api calls.""" status = None try: if machine_type == TYPE_VM: status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() elif machine_type == TYPE_CONTAINER: status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() except (ResourceException, requests.exceptions.ConnectionError): return None return status class ProxmoxEntity(CoordinatorEntity): """Represents any entity created for the Proxmox VE platform.""" def __init__( self, coordinator: DataUpdateCoordinator, unique_id, name, icon, host_name, node_name, vm_id=None, ): """Initialize the Proxmox entity.""" super().__init__(coordinator) self.coordinator = coordinator self._unique_id = unique_id self._name = name self._host_name = host_name self._icon = icon self._available = True self._node_name = node_name self._vm_id = vm_id self._state = None @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" return self._unique_id @property def name(self) -> str: """Return the name of the entity.""" return self._name @property def icon(self) -> str: """Return the mdi icon of the entity.""" return self._icon @property def available(self) -> bool: """Return True if entity is available.""" return self.coordinator.last_update_success and self._available class ProxmoxClient: """A wrapper for the proxmoxer ProxmoxAPI client.""" def __init__(self, host, port, user, realm, password, verify_ssl): """Initialize the ProxmoxClient.""" self._host = host self._port = port self._user = user self._realm = realm self._password = password self._verify_ssl = verify_ssl self._proxmox = None self._connection_start_time = None def build_client(self): """Construct the ProxmoxAPI client. Allows inserting the realm within the `user` value.""" if "@" in self._user: user_id = self._user else: user_id = f"{self._user}@{self._realm}" self._proxmox = ProxmoxAPI( self._host, port=self._port, user=user_id, password=self._password, verify_ssl=self._verify_ssl, ) def get_api_client(self): """Return the ProxmoxAPI client.""" return self._proxmox