Skip to content

Commit 23888bc

Browse files
caladdbsanders
authored andcommitted
FEATURE: Ansible module to manage Stacki networks
An Ansible module for adding, editing, and removing Stacki networks. The module takes these parameters: `name` - The name of the network to manage `address` - The IP address space assigned to the network `mask` - The network mask for the network space `gateway` - The IP for the gateway in this network space `mtu` - The MTU for the network space `zone` - The DNS domain (zone) for this network `dns` - Should the network will be included in the builtin DNS server `pxe` - Should the network be managed by the builtin DHCP/PXE server `state` - If present, then a network will be added (if needed) and options are set to match. If absent, then the network will be removed. Example playbook: ``` --- - hosts: localhost tasks: - name: Add a network stacki_network: name: test address: 10.10.0.0 mask: 255.255.255.0 gateway: 10.10.0.1 mtu: 1500 zone: foo.com dns: yes pxe: yes register: result - name: Add network output debug: var: result - name: Remove a network stacki_network: name: test state: absent register: result - name: Remove network output debug: var: result ``` Output of the debug commands, showing the structure of the data returned: ``` TASK [Add network output] ********************************************************************************************** ok: [localhost] => { "result": { "changed": true, "failed": false } } TASK [Remove network output] ******************************************************************************************* ok: [localhost] => { "result": { "changed": true, "failed": false } } ```
1 parent 1f6ccb6 commit 23888bc

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# @copyright@
2+
# Copyright (c) 2006 - 2020 Teradata
3+
# All rights reserved. Stacki(r) v5.x stacki.com
4+
# https://github.com/Teradata/stacki/blob/master/LICENSE.txt
5+
# @copyright@
6+
7+
DOCUMENTATION = """
8+
module: stacki_network
9+
short_description: Manage Stacki networks
10+
description:
11+
- Add, modify, and remove Stacki networks
12+
13+
options:
14+
name:
15+
description:
16+
- The name of the network to manage
17+
required: true
18+
type: str
19+
20+
address:
21+
description:
22+
- The IP address space assigned to the network
23+
required: Only for new networks
24+
type: str
25+
26+
mask:
27+
description:
28+
- The network mask for the network space
29+
required: Only for new networks
30+
type: str
31+
32+
gateway:
33+
description:
34+
- The IP for the gateway in this network space
35+
required: false
36+
type: str
37+
38+
mtu:
39+
description:
40+
- The MTU for the network space
41+
required: false
42+
type: int
43+
44+
zone:
45+
description:
46+
- The DNS domain (zone) for this network
47+
type: str
48+
default: The network name
49+
50+
dns:
51+
description:
52+
- Should the network will be included in the builtin DNS server
53+
type: bool
54+
default: no
55+
56+
pxe:
57+
description:
58+
- Should the network be managed by the builtin DHCP/PXE server
59+
type: bool
60+
default: no
61+
62+
state:
63+
description:
64+
- If present, then a network will be added (if needed) and options are set to match
65+
- If absent, then the network will be removed
66+
type: str
67+
choices: [ absent, present ]
68+
default: present
69+
"""
70+
71+
EXAMPLES = """
72+
- name: Add a network
73+
stacki_network:
74+
name: primary
75+
address: 192.168.0.0
76+
mask: 255.255.255.0
77+
gateway: 192.168.0.1
78+
mtu: 1500
79+
zone: local
80+
dns: yes
81+
pxe: yes
82+
83+
- name: Remove a network
84+
stacki_network:
85+
name: primary
86+
state: absent
87+
"""
88+
89+
RETURN = """ # """
90+
91+
from ansible.module_utils.basic import AnsibleModule
92+
from ansible.module_utils.stacki import run_stack_command, StackCommandError
93+
94+
95+
def main():
96+
# Define the arguments for this module
97+
argument_spec = dict(
98+
name=dict(type="str", required=True),
99+
address=dict(type="str", required=False),
100+
mask=dict(type="str", required=False),
101+
gateway=dict(type="str", required=False),
102+
mtu=dict(type="int", required=False),
103+
zone=dict(type="str", required=False),
104+
dns=dict(type="bool", required=False),
105+
pxe=dict(type="bool", required=False),
106+
state=dict(type="str", default="present", choices=["absent", "present"])
107+
)
108+
109+
# Create our module object
110+
module = AnsibleModule(
111+
argument_spec=argument_spec,
112+
supports_check_mode=True
113+
)
114+
115+
# Initialize a blank result
116+
result = {
117+
"changed": False
118+
}
119+
120+
# Bail if the user is just checking syntax of their playbook
121+
if module.check_mode:
122+
module.exit_json(**result)
123+
124+
# Fetch our network info from Stacki
125+
try:
126+
networks = run_stack_command("list.network", [module.params["name"]])
127+
except StackCommandError as e:
128+
# If box doesn't exist, it will raise an error
129+
networks = []
130+
131+
if len(networks) > 1:
132+
# No more than one network should match
133+
module.fail_json(msg="error - more than one network matches name", **result)
134+
135+
try:
136+
# Are we adding or removing?
137+
if module.params["state"] == "present":
138+
if len(networks) == 0:
139+
# Adding a new network from scratch
140+
args = [module.params["name"]]
141+
for field in ("address", "mask", "gateway", "mtu", "zone", "dns", "pxe"):
142+
if module.params[field]:
143+
args.append(f"{field}={module.params[field]}")
144+
145+
run_stack_command("add.network", args)
146+
result["changed"] = True
147+
148+
else:
149+
# We are modifying an existing network
150+
for field in ("address", "mask", "gateway", "mtu", "zone", "dns", "pxe"):
151+
# Did the playbook specify a value for the field?
152+
if module.params[field] is not None:
153+
# Do we need to modify the field?
154+
if module.params[field] != networks[0][field]:
155+
run_stack_command(f"set.network.{field}", [
156+
module.params["name"], f"{field}={module.params[field]}"
157+
])
158+
result["changed"] = True
159+
160+
else:
161+
# Only remove a network that actually exists
162+
if len(networks):
163+
run_stack_command("remove.network", [module.params["name"]])
164+
result["changed"] = True
165+
166+
except StackCommandError as e:
167+
# Fetching the data failed
168+
module.fail_json(msg=e.message, **result)
169+
170+
# Return our data
171+
module.exit_json(**result)
172+
173+
174+
if __name__ == "__main__":
175+
main()
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import json
2+
3+
4+
class TestStackiNetwork:
5+
def test_add_network(self, host, run_ansible_module):
6+
# Add the network
7+
result = run_ansible_module(
8+
"stacki_network", name="test", address="10.10.0.0", mask="255.255.255.0",
9+
gateway="10.10.0.1", mtu=1000, zone="foo.com", dns=True, pxe=True
10+
)
11+
12+
assert result.status == "CHANGED"
13+
assert result.data["changed"] == True
14+
15+
# Check that it is there now
16+
result = host.run("stack list network test output-format=json")
17+
assert result.rc == 0
18+
assert json.loads(result.stdout) == [
19+
{
20+
"network": "test",
21+
"address": "10.10.0.0",
22+
"mask": "255.255.255.0",
23+
"gateway": "10.10.0.1",
24+
"mtu": 1000,
25+
"zone": "foo.com",
26+
"dns": True,
27+
"pxe": True
28+
}
29+
]
30+
31+
# Test idempotency by adding again
32+
result = run_ansible_module("stacki_network", name="test", address="10.10.0.0", mask="255.255.255.0")
33+
34+
assert result.status == "SUCCESS"
35+
assert result.data["changed"] == False
36+
37+
def test_modify_network(self, host, run_ansible_module):
38+
# Add the network
39+
result = run_ansible_module(
40+
"stacki_network", name="test", address="10.10.0.0", mask="255.255.255.0"
41+
)
42+
43+
assert result.status == "CHANGED"
44+
assert result.data["changed"] == True
45+
46+
# Check that it is there now
47+
result = host.run("stack list network test output-format=json")
48+
assert result.rc == 0
49+
assert json.loads(result.stdout) == [
50+
{
51+
"network": "test",
52+
"address": "10.10.0.0",
53+
"mask": "255.255.255.0",
54+
"gateway": "",
55+
"mtu": None,
56+
"zone": "test",
57+
"dns": False,
58+
"pxe": False
59+
}
60+
]
61+
62+
# Now change all the fields of a network
63+
result = run_ansible_module(
64+
"stacki_network", name="test", address="10.20.0.0", mask="255.255.0.0",
65+
gateway="10.20.0.1", mtu=1000, zone="foo.com", dns=True, pxe=True
66+
)
67+
assert result.status == "CHANGED"
68+
assert result.data["changed"] == True
69+
70+
71+
# Check that the fields changed
72+
result = host.run("stack list network test output-format=json")
73+
assert result.rc == 0
74+
assert json.loads(result.stdout) == [
75+
{
76+
"network": "test",
77+
"address": "10.20.0.0",
78+
"mask": "255.255.0.0",
79+
"gateway": "10.20.0.1",
80+
"mtu": 1000,
81+
"zone": "foo.com",
82+
"dns": True,
83+
"pxe": True
84+
}
85+
]
86+
87+
# Test idempotency by adding again
88+
result = run_ansible_module(
89+
"stacki_network", name="test", address="10.20.0.0", mask="255.255.0.0",
90+
gateway="10.20.0.1", mtu=1000, zone="foo.com", dns=True, pxe=True
91+
)
92+
assert result.status == "SUCCESS"
93+
assert result.data["changed"] == False
94+
95+
def test_remove_network(self, add_network, host, run_ansible_module):
96+
# Remove the network
97+
result = run_ansible_module("stacki_network", name="test", state="absent")
98+
99+
assert result.status == "CHANGED"
100+
assert result.data["changed"] == True
101+
102+
# And confirm it is gone
103+
result = host.run("stack list network test")
104+
assert result.rc == 255
105+
assert "not a valid network" in result.stderr
106+
107+
# Test idempotency by removing it again
108+
result = run_ansible_module("stacki_network", name="test", state="absent")
109+
110+
assert result.status == "SUCCESS"
111+
assert result.data["changed"] == False
112+
113+
def test_bad_name(self, add_network, run_ansible_module):
114+
result = run_ansible_module("stacki_network", name="%", state="absent")
115+
116+
assert result.status == "FAILED!"
117+
assert result.data["changed"] == False
118+
119+
assert "error" in result.data["msg"]
120+
assert "more than one network matches name" in result.data["msg"]

0 commit comments

Comments
 (0)