fixup! wip: dynamic inventory
parent
9e7eff5e9e
commit
3a0ed3dbd5
|
@ -1,7 +1,7 @@
|
|||
# vim: set ft=dosini :
|
||||
|
||||
[defaults]
|
||||
inventory = hosts
|
||||
inventory = hosts, hosts.pyrocufflink.yml
|
||||
|
||||
callback_plugins = plugins/callback
|
||||
inventory_plugins = plugins/inventory
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
plugin: pyrocufflink
|
||||
uri:
|
||||
- qemu+ssh://vmhost0.pyrocufflink.blue/system
|
||||
- qemu+ssh://vmhost1.pyrocufflink.blue/system
|
||||
read_only: true
|
||||
dns_domain: pyrocufflink.blue
|
||||
exclude_off: true
|
||||
exclude_os:
|
||||
- http://fedoraproject.org/coreos/stable
|
||||
log_excluded: true
|
|
@ -1,11 +1,14 @@
|
|||
import enum
|
||||
import functools
|
||||
import logging
|
||||
from typing import cast, Any, Optional
|
||||
from xml.etree import ElementTree as etree
|
||||
|
||||
import libvirt
|
||||
import yaml
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
name: pyrocufflink
|
||||
extends_documentation_fragment:
|
||||
|
@ -22,87 +25,222 @@ options:
|
|||
Token that ensures this is a source file for the 'pyrocufflink' plugin.
|
||||
required: True
|
||||
choices: ['pyrocufflink']
|
||||
uri:
|
||||
description: libvirt connection URI
|
||||
type: list
|
||||
required: true
|
||||
read_only:
|
||||
description: Open a read-only connection to libvirt
|
||||
type: boolean
|
||||
default: false
|
||||
dns_domain:
|
||||
description: DNS doman to append to single-label VM names
|
||||
type: string
|
||||
exclude_off:
|
||||
description: Exclude libvirt domains that are powered off
|
||||
type: boolean
|
||||
default: false
|
||||
exclude_os:
|
||||
description: Exclude domains running specified operating systems
|
||||
type: list
|
||||
default: []
|
||||
log_excluded:
|
||||
description:
|
||||
Print a log message about excluded domains, useful for troubleshooting
|
||||
type: boolean
|
||||
default: false
|
||||
'''
|
||||
|
||||
|
||||
log = logging.getLogger('ansible.plugins.inventory.pyrocufflink')
|
||||
|
||||
METADATA_XMLNS = 'http://du5t1n.me/xmlns/libvirt/metadata/'
|
||||
|
||||
DOMAIN_STATES = {
|
||||
0: 'no state',
|
||||
1: 'the domain is running',
|
||||
2: 'the domain is blocked on resource',
|
||||
3: 'the domain is paused by user',
|
||||
4: 'the domain is being shut down',
|
||||
5: 'the domain is shut off',
|
||||
6: 'the domain is crashed',
|
||||
7: 'the domain is suspended by guest power management',
|
||||
}
|
||||
|
||||
# Remove the default error handler, which prints to stderr
|
||||
libvirt.registerErrorHandler(lambda *_: None, None)
|
||||
|
||||
|
||||
class XMLNS:
|
||||
dch = 'http://du5t1n.me/xmlns/libvirt/metadata/'
|
||||
libosinfo = 'http://libosinfo.org/xmlns/libvirt/domain/1.0'
|
||||
|
||||
|
||||
class DomainState(enum.IntEnum):
|
||||
nostate = 0
|
||||
running = 1
|
||||
blockde = 2
|
||||
paused = 3
|
||||
shutdown = 4
|
||||
shutoff = 5
|
||||
crashed = 6
|
||||
pmsuspend = 7
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
NAME = 'pyrocufflink'
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super().parse(inventory, loader, path, cache=cache)
|
||||
config_data = self._read_config_data(path)
|
||||
# set _options from config data
|
||||
self._consume_options(config_data)
|
||||
|
||||
uri_list = self.get_option('uri')
|
||||
if not isinstance(uri_list, list):
|
||||
uri_list = [uri_list]
|
||||
read_only = cast(bool, self.get_option('read_only'))
|
||||
dns_domain = self.get_option('dns_domain')
|
||||
exclude_off = cast(bool, self.get_option('exclude_off'))
|
||||
exclude_os = cast(list[str], self.get_option('exclude_os'))
|
||||
log_excluded = cast(bool, self.get_option('log_excluded'))
|
||||
|
||||
assert self.inventory
|
||||
conn = libvirt.open()
|
||||
for dom in conn.listAllDomains():
|
||||
v = {}
|
||||
|
||||
name = v['name'] = dom.name()
|
||||
try:
|
||||
v['title'] = dom.metadata(1, None)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug('Could not get title for domain %s: %s', name, e)
|
||||
inventory_hostname = name
|
||||
for uri in uri_list:
|
||||
if read_only:
|
||||
conn = libvirt.openReadOnly(uri)
|
||||
else:
|
||||
inventory_hostname = v['title']
|
||||
self.inventory.add_host(inventory_hostname)
|
||||
|
||||
try:
|
||||
v['description'] = dom.metadata(0, None)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug('Could not get description for %s: %s', name, e)
|
||||
|
||||
try:
|
||||
metadata = dom.metadata(2, METADATA_XMLNS)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug(
|
||||
'Could not get extended domain metadata for %s: %s',
|
||||
name,
|
||||
e,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
v['metadata'] = parse_metadata(metadata)
|
||||
except Exception as e:
|
||||
log.error('Failed to parse metadata for %s: %s', name, e)
|
||||
|
||||
state, reason = dom.state()
|
||||
v['state_code'] = state
|
||||
v['state_reason_code'] = reason
|
||||
try:
|
||||
v['state'] = DOMAIN_STATES[state]
|
||||
except KeyError:
|
||||
pass
|
||||
if state == 1:
|
||||
try:
|
||||
guest_info = dom.guestInfo()
|
||||
except libvirt.libvirtError as e:
|
||||
log.error('Could not get guest info for %s: %s', name, e)
|
||||
conn = libvirt.open(uri)
|
||||
for dom in conn.listAllDomains():
|
||||
host = Host(dom)
|
||||
state = host.get_state()[0]
|
||||
if state == DomainState.shutoff and exclude_off:
|
||||
if log_excluded:
|
||||
log.warning(
|
||||
'Excluding libvirt domain %s in state %r',
|
||||
host.name,
|
||||
DomainState.shutoff.name,
|
||||
)
|
||||
continue
|
||||
if host.os_id in exclude_os:
|
||||
if log_excluded:
|
||||
log.warning(
|
||||
'Excluding libvirt domain %s with OS %r',
|
||||
host.name,
|
||||
host.os_id,
|
||||
)
|
||||
continue
|
||||
if host.title:
|
||||
inventory_hostname = host.title
|
||||
else:
|
||||
self.inventory.set_variable(name, 'guest_info', guest_info)
|
||||
self.inventory.set_variable(inventory_hostname, 'libvirt', v)
|
||||
inventory_hostname = host.name
|
||||
if dns_domain and '.' not in inventory_hostname:
|
||||
inventory_hostname = f'{inventory_hostname}.{dns_domain}'
|
||||
self.inventory.add_host(inventory_hostname)
|
||||
for group in host.groups:
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_host(inventory_hostname, group)
|
||||
self.inventory.set_variable(
|
||||
inventory_hostname, 'libvirt_uri', uri
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
inventory_hostname, 'libvirt', host.variables(read_only)
|
||||
)
|
||||
|
||||
|
||||
def parse_metadata(metadata: str):
|
||||
tree = etree.fromstring(metadata)
|
||||
return yaml.safe_load(tree.text)
|
||||
class Host:
|
||||
|
||||
def __init__(self, domain: libvirt.virDomain) -> None:
|
||||
self.domain = domain
|
||||
|
||||
@functools.cached_property
|
||||
def description(self) -> Optional[str]:
|
||||
try:
|
||||
return self.domain.metadata(0, None)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug(
|
||||
'Could not get description for domain %s: %s', self.name, e
|
||||
)
|
||||
return None
|
||||
|
||||
@functools.cached_property
|
||||
def groups(self) -> list[str]:
|
||||
grouplist = []
|
||||
if self.metadata:
|
||||
if groups := self.metadata.find('groups'):
|
||||
for elem in groups.iter('group'):
|
||||
if group_name := elem.get('name'):
|
||||
grouplist.append(group_name)
|
||||
return grouplist
|
||||
|
||||
@functools.cached_property
|
||||
def libosinfo(self) -> Optional[etree.Element]:
|
||||
try:
|
||||
metadata = self.domain.metadata(2, XMLNS.libosinfo)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug(
|
||||
'Could not get extended domain metadata for %s: %s',
|
||||
self.name,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
return etree.fromstring(metadata)
|
||||
|
||||
@functools.cached_property
|
||||
def metadata(self) -> Optional[etree.Element]:
|
||||
try:
|
||||
metadata = self.domain.metadata(2, XMLNS.dch)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug(
|
||||
'Could not get extended domain metadata for %s: %s',
|
||||
self.name,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
return etree.fromstring(metadata)
|
||||
|
||||
@functools.cached_property
|
||||
def name(self) -> str:
|
||||
return self.domain.name()
|
||||
|
||||
@functools.cached_property
|
||||
def os_id(self) -> Optional[str]:
|
||||
if self.libosinfo is None:
|
||||
return
|
||||
if (os := self.libosinfo.find('os')) is not None:
|
||||
return os.get('id')
|
||||
|
||||
@functools.cached_property
|
||||
def title(self) -> Optional[str]:
|
||||
try:
|
||||
return self.domain.metadata(1, None)
|
||||
except libvirt.libvirtError as e:
|
||||
log.debug('Could not get title for domain %s: %s', self.name, e)
|
||||
return None
|
||||
|
||||
def get_guest_info(self) -> Optional[dict[str, str]]:
|
||||
try:
|
||||
return self.domain.guestInfo()
|
||||
except libvirt.libvirtError as e:
|
||||
log.error('Could not get guest info for %s: %s', self.name, e)
|
||||
|
||||
def get_state(self) -> tuple[int, int]:
|
||||
state, reason = self.domain.state()
|
||||
return state, reason
|
||||
|
||||
def variables(self, read_only: bool = False) -> dict[str, Any]:
|
||||
values = {}
|
||||
|
||||
if title := self.title:
|
||||
values['title'] = title
|
||||
if description := self.description:
|
||||
values['description'] = description
|
||||
|
||||
if self.os_id:
|
||||
values['os_id'] = self.os_id
|
||||
|
||||
state_code, reason_code = self.get_state()
|
||||
values['state_code'] = state_code
|
||||
values['reason_code'] = reason_code
|
||||
try:
|
||||
state = DomainState(state_code)
|
||||
except KeyError:
|
||||
log.warning(
|
||||
'Unknown state for domain %s: %s:', self.name, state_code
|
||||
)
|
||||
state = None
|
||||
else:
|
||||
values['state'] = state.name
|
||||
|
||||
if not read_only and state is DomainState.running:
|
||||
if guest_info := self.get_guest_info():
|
||||
values['guest_info'] = guest_info
|
||||
|
||||
return values
|
||||
|
|
Loading…
Reference in New Issue