From 829e0a7c4268a643ea31969ae25c30305d58680c Mon Sep 17 00:00:00 2001 From: Jonas <5180118+K4ds3@users.noreply.github.com> Date: Fri, 22 Nov 2019 23:03:41 +0100 Subject: [PATCH] Add Proxmox VE integration (#27315) * Added the Proxmox VE integration * Fixed code as described in PR #27315 * Fixed small linting error * Fix code as described in PR #27315 code review * Improve code as described in PR #27315 --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/proxmoxve/__init__.py | 154 ++++++++++++++++++ .../components/proxmoxve/binary_sensor.py | 112 +++++++++++++ .../components/proxmoxve/manifest.json | 8 + requirements_all.txt | 3 + 6 files changed, 279 insertions(+) create mode 100644 homeassistant/components/proxmoxve/__init__.py create mode 100644 homeassistant/components/proxmoxve/binary_sensor.py create mode 100644 homeassistant/components/proxmoxve/manifest.json diff --git a/.coveragerc b/.coveragerc index ab4e91e5efd..a241c9ab79c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -526,6 +526,7 @@ omit = homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py + homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 005c3e3489e..6b4b3e6a2f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -239,6 +239,7 @@ homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike +homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py new file mode 100644 index 00000000000..246dc2d48ad --- /dev/null +++ b/homeassistant/components/proxmoxve/__init__.py @@ -0,0 +1,154 @@ +"""Support for Proxmox VE.""" +from enum import Enum +import logging +import time + +from proxmoxer import ProxmoxAPI +from proxmoxer.backends.https import AuthenticationError +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "proxmoxve" +PROXMOX_CLIENTS = "proxmox_clients" +CONF_REALM = "realm" +CONF_NODE = "node" +CONF_NODES = "nodes" +CONF_VMS = "vms" +CONF_CONTAINERS = "containers" + +DEFAULT_PORT = 8006 +DEFAULT_REALM = "pam" +DEFAULT_VERIFY_SSL = True + +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, +) + + +def setup(hass, config): + """Set up the component.""" + + # Create API Clients for later use + 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] + + 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 + + hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client + + if hass.data[PROXMOX_CLIENTS]: + hass.helpers.discovery.load_platform( + "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config + ) + return True + + return False + + +class ProxmoxItemType(Enum): + """Represents the different types of machines in Proxmox.""" + + qemu = 0 + lxc = 1 + + +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.""" + + self._proxmox = ProxmoxAPI( + self._host, + port=self._port, + user=f"{self._user}@{self._realm}", + password=self._password, + verify_ssl=self._verify_ssl, + ) + + self._connection_start_time = time.time() + + def get_api_client(self): + """Return the ProxmoxAPI client and rebuild it if necessary.""" + + connection_age = time.time() - self._connection_start_time + + # Workaround for the Proxmoxer bug where the connection stops working after some time + if connection_age > 30 * 60: + self.build_client() + + return self._proxmox diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py new file mode 100644 index 00000000000..15b1f1483e1 --- /dev/null +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -0,0 +1,112 @@ +"""Binary sensor to read Proxmox VE data.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT + +from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType + +ATTRIBUTION = "Data provided by Proxmox VE" +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + sensors = [] + + for entry in discovery_info["entries"]: + port = entry[CONF_PORT] + + for node in entry[CONF_NODES]: + for virtual_machine in node[CONF_VMS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.qemu, + virtual_machine, + ) + ) + + for container in node[CONF_CONTAINERS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.lxc, + container, + ) + ) + + add_entities(sensors, True) + + +class ProxmoxBinarySensor(BinarySensorDevice): + """A binary sensor for reading Proxmox VE data.""" + + def __init__(self, proxmox_client, item_node, item_type, item_id): + """Initialize the binary sensor.""" + self._proxmox_client = proxmox_client + self._item_node = item_node + self._item_type = item_type + self._item_id = item_id + + self._vmname = None + self._name = None + + self._state = None + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if VM/container is running.""" + return self._state + + @property + def device_state_attributes(self): + """Return device attributes of the entity.""" + return { + "node": self._item_node, + "vmid": self._item_id, + "vmname": self._vmname, + "type": self._item_type.name, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + def update(self): + """Check if the VM/Container is running.""" + item = self.poll_item() + + if item is None: + _LOGGER.warning("Failed to poll VM/container %s", self._item_id) + return + + self._state = item["status"] == "running" + + def poll_item(self): + """Find the VM/Container with the set item_id.""" + items = ( + self._proxmox_client.get_api_client() + .nodes(self._item_node) + .get(self._item_type.name) + ) + item = next( + (item for item in items if item["vmid"] == str(self._item_id)), None + ) + + if item is None: + _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) + return None + + if self._vmname is None: + self._vmname = item["name"] + + if self._name is None: + self._name = f"{self._item_node} {self._vmname} running" + + return item diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json new file mode 100644 index 00000000000..9c03038a630 --- /dev/null +++ b/homeassistant/components/proxmoxve/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "proxmoxve", + "name": "Proxmox VE", + "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "dependencies": [], + "codeowners": ["@k4ds3"], + "requirements": ["proxmoxer==1.0.3"] + } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 3fd222ac268..7d7852add97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,6 +1023,9 @@ prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 +# homeassistant.components.proxmoxve +proxmoxer==1.0.3 + # homeassistant.components.systemmonitor psutil==5.6.5 -- GitLab