321 lines
9.5 KiB
Python
321 lines
9.5 KiB
Python
"""Support for Proxmox VE."""
|
|
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,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.update_coordinator import (
|
|
CoordinatorEntity,
|
|
DataUpdateCoordinator,
|
|
)
|
|
|
|
PLATFORMS = ["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: dict):
|
|
"""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
|
|
|
|
hass.data[PROXMOX_CLIENTS][host] = proxmox_client
|
|
|
|
await hass.async_add_executor_job(build_client)
|
|
|
|
coordinators = hass.data[DOMAIN][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(
|
|
hass.helpers.discovery.async_load_platform(
|
|
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
|