Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Proxmox VE."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from typing import Any
7 
8 from proxmoxer import AuthenticationError, ProxmoxAPI
9 from proxmoxer.core import ResourceException
10 import requests.exceptions
11 from requests.exceptions import ConnectTimeout, SSLError
12 import voluptuous as vol
13 
14 from homeassistant.const import (
15  CONF_HOST,
16  CONF_PASSWORD,
17  CONF_PORT,
18  CONF_USERNAME,
19  CONF_VERIFY_SSL,
20  Platform,
21 )
22 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers.discovery import async_load_platform
25 from homeassistant.helpers.typing import ConfigType
26 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
27 
28 from .const import (
29  _LOGGER,
30  CONF_CONTAINERS,
31  CONF_NODE,
32  CONF_NODES,
33  CONF_REALM,
34  CONF_VMS,
35  COORDINATORS,
36  DEFAULT_PORT,
37  DEFAULT_REALM,
38  DEFAULT_VERIFY_SSL,
39  DOMAIN,
40  PROXMOX_CLIENTS,
41  TYPE_CONTAINER,
42  TYPE_VM,
43  UPDATE_INTERVAL,
44 )
45 
46 PLATFORMS = [Platform.BINARY_SENSOR]
47 
48 CONFIG_SCHEMA = vol.Schema(
49  {
50  DOMAIN: vol.All(
51  cv.ensure_list,
52  [
53  vol.Schema(
54  {
55  vol.Required(CONF_HOST): cv.string,
56  vol.Required(CONF_USERNAME): cv.string,
57  vol.Required(CONF_PASSWORD): cv.string,
58  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
59  vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string,
60  vol.Optional(
61  CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL
62  ): cv.boolean,
63  vol.Required(CONF_NODES): vol.All(
64  cv.ensure_list,
65  [
66  vol.Schema(
67  {
68  vol.Required(CONF_NODE): cv.string,
69  vol.Optional(CONF_VMS, default=[]): [
70  cv.positive_int
71  ],
72  vol.Optional(CONF_CONTAINERS, default=[]): [
73  cv.positive_int
74  ],
75  }
76  )
77  ],
78  ),
79  }
80  )
81  ],
82  )
83  },
84  extra=vol.ALLOW_EXTRA,
85 )
86 
87 
88 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
89  """Set up the platform."""
90  hass.data.setdefault(DOMAIN, {})
91 
92  def build_client() -> ProxmoxAPI:
93  """Build the Proxmox client connection."""
94  hass.data[PROXMOX_CLIENTS] = {}
95 
96  for entry in config[DOMAIN]:
97  host = entry[CONF_HOST]
98  port = entry[CONF_PORT]
99  user = entry[CONF_USERNAME]
100  realm = entry[CONF_REALM]
101  password = entry[CONF_PASSWORD]
102  verify_ssl = entry[CONF_VERIFY_SSL]
103 
104  hass.data[PROXMOX_CLIENTS][host] = None
105 
106  try:
107  # Construct an API client with the given data for the given host
108  proxmox_client = ProxmoxClient(
109  host, port, user, realm, password, verify_ssl
110  )
111  proxmox_client.build_client()
112  except AuthenticationError:
113  _LOGGER.warning(
114  "Invalid credentials for proxmox instance %s:%d", host, port
115  )
116  continue
117  except SSLError:
118  _LOGGER.error(
119  (
120  "Unable to verify proxmox server SSL. "
121  'Try using "verify_ssl: false" for proxmox instance %s:%d'
122  ),
123  host,
124  port,
125  )
126  continue
127  except ConnectTimeout:
128  _LOGGER.warning("Connection to host %s timed out during setup", host)
129  continue
130  except requests.exceptions.ConnectionError:
131  _LOGGER.warning("Host %s is not reachable", host)
132  continue
133 
134  hass.data[PROXMOX_CLIENTS][host] = proxmox_client
135 
136  await hass.async_add_executor_job(build_client)
137 
138  coordinators: dict[
139  str, dict[str, dict[int, DataUpdateCoordinator[dict[str, Any] | None]]]
140  ] = {}
141  hass.data[DOMAIN][COORDINATORS] = coordinators
142 
143  # Create a coordinator for each vm/container
144  for host_config in config[DOMAIN]:
145  host_name = host_config["host"]
146  coordinators[host_name] = {}
147 
148  proxmox_client = hass.data[PROXMOX_CLIENTS][host_name]
149 
150  # Skip invalid hosts
151  if proxmox_client is None:
152  continue
153 
154  proxmox = proxmox_client.get_api_client()
155 
156  for node_config in host_config["nodes"]:
157  node_name = node_config["node"]
158  node_coordinators = coordinators[host_name][node_name] = {}
159 
160  for vm_id in node_config["vms"]:
161  coordinator = create_coordinator_container_vm(
162  hass, proxmox, host_name, node_name, vm_id, TYPE_VM
163  )
164 
165  # Fetch initial data
166  await coordinator.async_refresh()
167 
168  node_coordinators[vm_id] = coordinator
169 
170  for container_id in node_config["containers"]:
171  coordinator = create_coordinator_container_vm(
172  hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER
173  )
174 
175  # Fetch initial data
176  await coordinator.async_refresh()
177 
178  node_coordinators[container_id] = coordinator
179 
180  for component in PLATFORMS:
181  await hass.async_create_task(
182  async_load_platform(hass, component, DOMAIN, {"config": config}, config)
183  )
184 
185  return True
186 
187 
189  hass: HomeAssistant,
190  proxmox: ProxmoxAPI,
191  host_name: str,
192  node_name: str,
193  vm_id: int,
194  vm_type: int,
195 ) -> DataUpdateCoordinator[dict[str, Any] | None]:
196  """Create and return a DataUpdateCoordinator for a vm/container."""
197 
198  async def async_update_data() -> dict[str, Any] | None:
199  """Call the api and handle the response."""
200 
201  def poll_api() -> dict[str, Any] | None:
202  """Call the api."""
203  return call_api_container_vm(proxmox, node_name, vm_id, vm_type)
204 
205  vm_status = await hass.async_add_executor_job(poll_api)
206 
207  if vm_status is None:
208  _LOGGER.warning(
209  "Vm/Container %s unable to be found in node %s", vm_id, node_name
210  )
211  return None
212 
213  return parse_api_container_vm(vm_status)
214 
215  return DataUpdateCoordinator(
216  hass,
217  _LOGGER,
218  name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
219  update_method=async_update_data,
220  update_interval=timedelta(seconds=UPDATE_INTERVAL),
221  )
222 
223 
224 def parse_api_container_vm(status: dict[str, Any]) -> dict[str, Any]:
225  """Get the container or vm api data and return it formatted in a dictionary.
226 
227  It is implemented in this way to allow for more data to be added for sensors
228  in the future.
229  """
230 
231  return {"status": status["status"], "name": status["name"]}
232 
233 
235  proxmox: ProxmoxAPI,
236  node_name: str,
237  vm_id: int,
238  machine_type: int,
239 ) -> dict[str, Any] | None:
240  """Make proper api calls."""
241  status = None
242 
243  try:
244  if machine_type == TYPE_VM:
245  status = proxmox.nodes(node_name).qemu(vm_id).status.current.get()
246  elif machine_type == TYPE_CONTAINER:
247  status = proxmox.nodes(node_name).lxc(vm_id).status.current.get()
248  except (ResourceException, requests.exceptions.ConnectionError):
249  return None
250 
251  return status
252 
253 
255  """A wrapper for the proxmoxer ProxmoxAPI client."""
256 
257  _proxmox: ProxmoxAPI
258 
259  def __init__(
260  self,
261  host: str,
262  port: int,
263  user: str,
264  realm: str,
265  password: str,
266  verify_ssl: bool,
267  ) -> None:
268  """Initialize the ProxmoxClient."""
269 
270  self._host_host = host
271  self._port_port = port
272  self._user_user = user
273  self._realm_realm = realm
274  self._password_password = password
275  self._verify_ssl_verify_ssl = verify_ssl
276 
277  def build_client(self) -> None:
278  """Construct the ProxmoxAPI client.
279 
280  Allows inserting the realm within the `user` value.
281  """
282 
283  if "@" in self._user_user:
284  user_id = self._user_user
285  else:
286  user_id = f"{self._user}@{self._realm}"
287 
288  self._proxmox_proxmox = ProxmoxAPI(
289  self._host_host,
290  port=self._port_port,
291  user=user_id,
292  password=self._password_password,
293  verify_ssl=self._verify_ssl_verify_ssl,
294  )
295 
296  def get_api_client(self) -> ProxmoxAPI:
297  """Return the ProxmoxAPI client."""
298  return self._proxmox_proxmox
None __init__(self, str host, int port, str user, str realm, str password, bool verify_ssl)
Definition: __init__.py:267
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:88
DataUpdateCoordinator[dict[str, Any]|None] create_coordinator_container_vm(HomeAssistant hass, ProxmoxAPI proxmox, str host_name, str node_name, int vm_id, int vm_type)
Definition: __init__.py:195
dict[str, Any]|None call_api_container_vm(ProxmoxAPI proxmox, str node_name, int vm_id, int machine_type)
Definition: __init__.py:239
dict[str, Any] parse_api_container_vm(dict[str, Any] status)
Definition: __init__.py:224
None async_load_platform(core.HomeAssistant hass, Platform|str component, str platform, DiscoveryInfoType|None discovered, ConfigType hass_config)
Definition: discovery.py:152