Skip to content

Commit bed224f

Browse files
JanaHochThulium-DrakeIamLunchbox
authored
new module proxmox_ipam_info (#177)
* proxmox_ipam_info: new module - get ip by vmid - get all ips under ipam * proxmox_ipam_info added docs * proxmox_ipam_info handle multiple ips for same vm * proxmox_ipam_info: fix sanity issues - Add proxmox_ipam_info to runtime.yml - Fix PEP8 issues - Add workaround for author string - Fix Docs * proxmox_ipam_info: Add Unit Test * proxmox_ipam_info: Apply suggestions from code review Added All the changes suggested by @IamLunchbox Co-authored-by: IamLunchbox <[email protected]> * proxmox_ipam_info: Fix minor issues in doc and test --------- Co-authored-by: Jeffrey van Pelt <[email protected]> Co-authored-by: IamLunchbox <[email protected]>
1 parent 513959c commit bed224f

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed

meta/runtime.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ action_groups:
2121
- proxmox_domain_info
2222
- proxmox_group
2323
- proxmox_group_info
24+
- proxmox_ipam_info
2425
- proxmox_kvm
2526
- proxmox_nic
2627
- proxmox_node
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright (c) 2025, Jana Hoch <[email protected]>
5+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
# SPDX-License-Identifier: GPL-3.0-or-later
7+
8+
from __future__ import absolute_import, division, print_function
9+
10+
__metaclass__ = type
11+
12+
DOCUMENTATION = r"""
13+
module: proxmox_ipam_info
14+
short_description: Retrieve information about IPAMs.
15+
version_added: "1.4.0"
16+
description:
17+
- Retrieve all IPs under IPAM and limit it by IP or IPAM.
18+
author: 'Jana Hoch <[email protected]> (!UNKNOWN)'
19+
options:
20+
ipam:
21+
description:
22+
- Limit results to a single IPAM.
23+
type: str
24+
vmid:
25+
description:
26+
- Get IP of a VM under IPAM.
27+
type: int
28+
29+
extends_documentation_fragment:
30+
- community.proxmox.proxmox.actiongroup_proxmox
31+
- community.proxmox.proxmox.documentation
32+
- community.proxmox.attributes
33+
- community.proxmox.attributes.info_module
34+
"""
35+
36+
EXAMPLES = r"""
37+
- name: Get all IPs under all IPAM
38+
community.proxmox.proxmox_ipam_info:
39+
api_user: "{{ pc.proxmox.api_user }}"
40+
api_token_id: "{{ pc.proxmox.api_token_id }}"
41+
api_token_secret: "{{ vault.proxmox.api_token_secret }}"
42+
api_host: "{{ pc.proxmox.api_host }}"
43+
validate_certs: false
44+
45+
- name: Get all IPs under pve IPAM
46+
community.proxmox.proxmox_ipam_info:
47+
api_user: "{{ pc.proxmox.api_user }}"
48+
api_token_id: "{{ pc.proxmox.api_token_id }}"
49+
api_token_secret: "{{ vault.proxmox.api_token_secret }}"
50+
api_host: "{{ pc.proxmox.api_host }}"
51+
validate_certs: false
52+
ipam: pve
53+
54+
- name: Get IP under IPAM of vmid 102
55+
community.proxmox.proxmox_ipam_info:
56+
api_user: "{{ pc.proxmox.api_user }}"
57+
api_token_id: "{{ pc.proxmox.api_token_id }}"
58+
api_token_secret: "{{ vault.proxmox.api_token_secret }}"
59+
api_host: "{{ pc.proxmox.api_host }}"
60+
validate_certs: false
61+
vmid: 102
62+
"""
63+
64+
RETURN = r"""
65+
ips:
66+
description: Filter by vmid
67+
returned: on success
68+
type: list
69+
elements: dict
70+
sample:
71+
[
72+
{
73+
"hostname": "ns3.proxmox.pc",
74+
"ip": "10.10.5.5",
75+
"mac": "BC:24:11:0E:72:04",
76+
"subnet": "10.10.5.0/24",
77+
"vmid": 102,
78+
"vnet": "test",
79+
"zone": "ans1"
80+
},
81+
{
82+
"hostname": "ns3.proxmox.pc",
83+
"ip": "10.10.0.8",
84+
"mac": "BC:24:11:F3:B1:81",
85+
"subnet": "10.10.0.0/24",
86+
"vmid": 102,
87+
"vnet": "test2",
88+
"zone": "test1"
89+
}
90+
]
91+
ipams:
92+
description: List of all IPAMs and IPs under them.
93+
returned: on success
94+
type: dict
95+
elements: dict
96+
sample:
97+
{
98+
"pve": [
99+
{
100+
"gateway": 1,
101+
"ip": "10.10.1.0",
102+
"subnet": "10.10.1.0/24",
103+
"vnet": "test2",
104+
"zone": "test1"
105+
},
106+
{
107+
"hostname": "ns3.proxmox.pc.test3",
108+
"ip": "10.10.0.6",
109+
"mac": "BC:24:11:0E:72:04",
110+
"subnet": "10.10.0.0/24",
111+
"vmid": 102,
112+
"vnet": "test2",
113+
"zone": "test1"
114+
},
115+
{
116+
"hostname": "ns4.proxmox.pc",
117+
"ip": "10.10.0.7",
118+
"mac": "BC:24:11:D5:CD:82",
119+
"subnet": "10.10.0.0/24",
120+
"vmid": 103,
121+
"vnet": "test2",
122+
"zone": "test1"
123+
},
124+
{
125+
"gateway": 1,
126+
"ip": "10.10.0.1",
127+
"subnet": "10.10.0.0/24",
128+
"vnet": "test2",
129+
"zone": "test1"
130+
},
131+
{
132+
"hostname": "ns2.proxmox.pc.test3",
133+
"ip": "10.10.0.5",
134+
"mac": "BC:24:11:86:77:56",
135+
"subnet": "10.10.0.0/24",
136+
"vmid": 101,
137+
"vnet": "test2",
138+
"zone": "test1"
139+
}
140+
]
141+
}
142+
"""
143+
144+
from ansible.module_utils.basic import AnsibleModule
145+
from ansible_collections.community.proxmox.plugins.module_utils.proxmox import (
146+
proxmox_auth_argument_spec,
147+
ProxmoxAnsible
148+
)
149+
150+
151+
def get_proxmox_args():
152+
return dict(
153+
ipam=dict(type="str", required=False),
154+
vmid=dict(type='int', required=False)
155+
)
156+
157+
158+
def get_ansible_module():
159+
module_args = proxmox_auth_argument_spec()
160+
module_args.update(get_proxmox_args())
161+
return AnsibleModule(argument_spec=module_args, supports_check_mode=True)
162+
163+
164+
class ProxmoxIpamInfoAnsible(ProxmoxAnsible):
165+
def __init__(self, module):
166+
super(ProxmoxIpamInfoAnsible, self).__init__(module)
167+
self.params = module.params
168+
169+
def run(self):
170+
vmid = self.params.get('vmid')
171+
ipam = self.params.get('ipam')
172+
if vmid:
173+
self.module.exit_json(
174+
changed=False, ips=self.get_ip_by_vmid(vmid)
175+
)
176+
177+
elif self.params.get('ipam'):
178+
if ipam not in self.get_ipams():
179+
self.module.fail_json(
180+
msg=f'IPAM {ipam} is not present'
181+
)
182+
else:
183+
self.module.exit_json(
184+
changed=False,
185+
ipams=self.get_ipam_status()[ipam]
186+
)
187+
else:
188+
self.module.exit_json(
189+
changed=False,
190+
ipams=self.get_ipam_status()
191+
)
192+
193+
def get_ipams(self):
194+
try:
195+
ipams = self.proxmox_api.cluster().sdn().ipams().get()
196+
return [ipam['ipam'] for ipam in ipams]
197+
except Exception as e:
198+
self.module.fail_json(
199+
msg=f'Failed to retrieve IPAM information {e}'
200+
)
201+
202+
def get_ipam_status(self):
203+
try:
204+
ipam_status = dict()
205+
ipams = self.get_ipams()
206+
for ipam_id in ipams:
207+
ipam_status[ipam_id] = self.proxmox_api.cluster().sdn().ipams(ipam_id).status().get()
208+
return ipam_status
209+
except Exception as e:
210+
self.module.fail_json(
211+
msg=f'Failed to retrieve IPAM status {e}'
212+
)
213+
214+
def get_ip_by_vmid(self, vmid):
215+
ipam_status = self.get_ipam_status()
216+
ips = []
217+
for ipam in ipam_status.values():
218+
for item in ipam:
219+
if item.get('vmid') == vmid:
220+
ips.append(item)
221+
return ips
222+
223+
224+
def main():
225+
module = get_ansible_module()
226+
proxmox = ProxmoxIpamInfoAnsible(module)
227+
228+
try:
229+
proxmox.run()
230+
except Exception as e:
231+
module.fail_json(msg=f'An error occurred: {e}')
232+
233+
234+
if __name__ == "__main__":
235+
main()
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2025, Jana Hoch <[email protected]>
4+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
7+
from __future__ import absolute_import, division, print_function
8+
9+
__metaclass__ = type
10+
11+
from unittest.mock import patch
12+
13+
import pytest
14+
15+
proxmoxer = pytest.importorskip("proxmoxer")
16+
17+
from ansible.module_utils import basic
18+
from ansible_collections.community.proxmox.plugins.modules import proxmox_ipam_info
19+
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
20+
ModuleTestCase,
21+
set_module_args,
22+
)
23+
import ansible_collections.community.proxmox.plugins.module_utils.proxmox as proxmox_utils
24+
25+
RAW_IPAM_STATUS = [
26+
{
27+
"subnet": "10.10.1.0/24",
28+
"vnet": "test2",
29+
"zone": "test1",
30+
"ip": "10.10.1.0",
31+
"gateway": 1
32+
},
33+
{
34+
"ip": "10.10.0.1",
35+
"gateway": 1,
36+
"vnet": "test2",
37+
"subnet": "10.10.0.0/24",
38+
"zone": "test1"
39+
},
40+
{
41+
"zone": "test1",
42+
"vnet": "test2",
43+
"subnet": "10.10.0.0/24",
44+
"mac": "BC:24:11:F3:B1:81",
45+
"vmid": 102,
46+
"hostname": "ns3.proxmox.pc",
47+
"ip": "10.10.0.8"
48+
},
49+
{
50+
"subnet": "10.10.0.0/24",
51+
"vnet": "test2",
52+
"zone": "test1",
53+
"ip": "10.10.0.7",
54+
"hostname": "ns4.proxmox.pc",
55+
"vmid": 103,
56+
"mac": "BC:24:11:D5:CD:82"
57+
},
58+
{
59+
"ip": "10.10.0.5",
60+
"hostname": "ns2.proxmox.pc.test3",
61+
"mac": "BC:24:11:86:77:56",
62+
"vmid": 101,
63+
"subnet": "10.10.0.0/24",
64+
"vnet": "test2",
65+
"zone": "test1"
66+
}
67+
]
68+
69+
RAW_IPAM = [
70+
{
71+
"ipam": "pve",
72+
"type": "pve",
73+
"digest": "da39a3ee5e6b4b0d3255bfef95601890afd80709"
74+
}
75+
]
76+
77+
78+
def exit_json(*args, **kwargs):
79+
"""function to patch over exit_json; package return data into an exception"""
80+
if 'changed' not in kwargs:
81+
kwargs['changed'] = False
82+
raise SystemExit(kwargs)
83+
84+
85+
def fail_json(*args, **kwargs):
86+
"""function to patch over fail_json; package return data into an exception"""
87+
kwargs['failed'] = True
88+
raise SystemExit(kwargs)
89+
90+
91+
def get_module_args(ipam=None, vmid=None):
92+
return {
93+
'api_host': 'host',
94+
'api_user': 'user',
95+
'api_password': 'password',
96+
'ipam': ipam,
97+
'vmid': vmid
98+
}
99+
100+
101+
class TestProxmoxIpamInfoModule(ModuleTestCase):
102+
def setUp(self):
103+
super(TestProxmoxIpamInfoModule, self).setUp()
104+
proxmox_utils.HAS_PROXMOXER = True
105+
self.module = proxmox_ipam_info
106+
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
107+
exit_json=exit_json,
108+
fail_json=fail_json)
109+
self.mock_module_helper.start()
110+
self.connect_mock = patch(
111+
"ansible_collections.community.proxmox.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
112+
).start()
113+
self.mock_ipam = self.connect_mock.return_value.cluster.return_value.sdn.return_value.ipams.return_value
114+
self.mock_ipam.get.return_value = RAW_IPAM
115+
self.mock_ipam.pve.return_value.status.return_value.get.return_value = RAW_IPAM_STATUS
116+
self.mock_ipam.status.return_value.get.return_value = RAW_IPAM_STATUS
117+
118+
def tearDown(self):
119+
self.connect_mock.stop()
120+
self.mock_module_helper.stop()
121+
super(TestProxmoxIpamInfoModule, self).tearDown()
122+
123+
def test_get_all_ipam_status(self):
124+
with pytest.raises(SystemExit) as exc_info:
125+
with set_module_args(get_module_args(ipam=None)):
126+
self.module.main()
127+
128+
result = exc_info.value.args[0]
129+
assert result["changed"] is False
130+
assert result["ipams"] == {'pve': RAW_IPAM_STATUS}
131+
132+
def test_get_all_ipam_pve_status(self):
133+
with pytest.raises(SystemExit) as exc_info:
134+
with set_module_args(get_module_args(ipam='pve')):
135+
self.module.main()
136+
137+
result = exc_info.value.args[0]
138+
assert result["changed"] is False
139+
assert result["ipams"] == RAW_IPAM_STATUS
140+
141+
def test_get_ip_by_vmid(self):
142+
with pytest.raises(SystemExit) as exc_info:
143+
with set_module_args(get_module_args(vmid=102)):
144+
self.module.main()
145+
146+
result = exc_info.value.args[0]
147+
assert result["changed"] is False
148+
assert result["ips"] == [x for x in RAW_IPAM_STATUS if x.get('vmid') == 102]

0 commit comments

Comments
 (0)