Skip to content

Commit 49e304c

Browse files
caladdbsanders
authored andcommitted
FEATURE: Ansible module to manage Stacki appliances
An Ansible module for adding, editing, and removing Stacki appliances. The module takes these parameters: `name` - The name of the appliance to manage `node` - The name of the root XML node for the appliance `state` - If present, then an appliance will be added (if needed) and the node attr set to match. If absent, then the appliance will be removed. Example playbook: ``` --- - hosts: localhost tasks: - name: Add an appliance stacki_appliance: name: test node: backend register: result - name: Add appliance output debug: var: result - name: Remove an appliance stacki_appliance: name: test state: absent register: result - name: Remove appliance output debug: var: result ``` Output of the debug commands, showing the structure of the data returned: ``` TASK [Add appliance output] ************************************************************************************************************************************ ok: [localhost] => { "result": { "changed": true, "failed": false } } TASK [Remove appliance output] ********************************************************************************************************************************* ok: [localhost] => { "result": { "changed": true, "failed": false } } ```
1 parent 0b573f0 commit 49e304c

File tree

2 files changed

+263
-0
lines changed

2 files changed

+263
-0
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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_appliance
9+
short_description: Manage Stacki appliances
10+
description:
11+
- Add, modify, and remove Stacki appliances
12+
13+
options:
14+
name:
15+
description:
16+
- The name of the appliance to manage
17+
required: true
18+
type: str
19+
20+
node:
21+
description:
22+
- The name of the root XML node for the appliance
23+
required: false
24+
type: str
25+
26+
state:
27+
description:
28+
- If present, then an appliance will be added (if needed) and node updated (if provided)
29+
- If absent, then the appliance will be removed
30+
type: str
31+
choices: [ absent, present ]
32+
default: present
33+
"""
34+
35+
EXAMPLES = """
36+
- name: Add an appliance
37+
stacki_appliance:
38+
name: test
39+
node: backend
40+
41+
- name: Remove an appliance
42+
stacki_appliance:
43+
name: test
44+
state: absent
45+
"""
46+
47+
RETURN = """ # """
48+
49+
from ansible.module_utils.basic import AnsibleModule
50+
from ansible.module_utils.stacki import run_stack_command, StackCommandError
51+
52+
53+
def main():
54+
# Define the arguments for this module
55+
argument_spec = dict(
56+
name=dict(type="str", required=True),
57+
node=dict(type="str", required=False),
58+
state=dict(type="str", default="present", choices=["absent", "present"])
59+
)
60+
61+
# Create our module object
62+
module = AnsibleModule(
63+
argument_spec=argument_spec,
64+
supports_check_mode=True
65+
)
66+
67+
# Initialize a blank result
68+
result = {
69+
"changed": False
70+
}
71+
72+
# Bail if the user is just checking syntax of their playbook
73+
if module.check_mode:
74+
module.exit_json(**result)
75+
76+
# Fetch our appliance info from Stacki
77+
try:
78+
appliances = run_stack_command("list.appliance", [module.params["name"]])
79+
except StackCommandError as e:
80+
# If appliance doesn't exist, it will raise an error
81+
appliances = []
82+
83+
if len(appliances) > 1:
84+
# No more than one appliance should match
85+
module.fail_json(msg="error - more than one appliance matches name", **result)
86+
87+
try:
88+
# Are we adding or removing?
89+
if module.params["state"] == "present":
90+
if len(appliances) == 0:
91+
# Adding a new appliance
92+
args = [module.params["name"]]
93+
if module.params["node"]:
94+
args.append("node="+module.params['node'])
95+
96+
run_stack_command("add.appliance", args)
97+
result["changed"] = True
98+
99+
else:
100+
# We are modifying an existing appliance
101+
if module.params["node"]:
102+
# Fetch the existing node attr
103+
attrs = run_stack_command(
104+
"list.appliance.attr", [module.params["name"], "attr=node"]
105+
)
106+
if len(attrs):
107+
existing_value = attrs[0]["value"]
108+
else:
109+
existing_value = None
110+
111+
# Change the attr if needed
112+
if module.params["node"] != existing_value:
113+
run_stack_command(f"set.appliance.attr", [
114+
module.params["name"], "attr=node", "value="+module.params["node"]
115+
])
116+
result["changed"] = True
117+
118+
else:
119+
# Only remove an appliance that actually exists
120+
if len(appliances):
121+
run_stack_command("remove.appliance", [module.params["name"]])
122+
result["changed"] = True
123+
124+
except StackCommandError as e:
125+
# Fetching the data failed
126+
module.fail_json(msg=e.message, **result)
127+
128+
# Return our data
129+
module.exit_json(**result)
130+
131+
132+
if __name__ == "__main__":
133+
main()
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import json
2+
3+
4+
class TestStackiAppliance:
5+
def test_add_appliance(self, host, run_ansible_module):
6+
# Add the appliance
7+
result = run_ansible_module("stacki_appliance", name="test")
8+
9+
assert result.status == "CHANGED"
10+
assert result.data["changed"] == True
11+
12+
# Check that it is there now
13+
result = host.run("stack list appliance test output-format=json")
14+
assert result.rc == 0
15+
assert json.loads(result.stdout) == [
16+
{
17+
"appliance": "test",
18+
"public": "yes"
19+
}
20+
]
21+
22+
# Test idempotency by adding again
23+
result = run_ansible_module("stacki_appliance", name="test")
24+
25+
assert result.status == "SUCCESS"
26+
assert result.data["changed"] == False
27+
28+
def test_modify_appliance_no_node(self, host, run_ansible_module):
29+
# Add the appliance without giving it a node parameter
30+
result = run_ansible_module("stacki_appliance", name="test")
31+
32+
assert result.status == "CHANGED"
33+
assert result.data["changed"] == True
34+
35+
# Confirm there is no node attr for the appliance
36+
result = host.run("stack list appliance attr test attr=node")
37+
assert result.rc == 0
38+
assert result.stdout == ""
39+
40+
# Now change the node
41+
result = run_ansible_module("stacki_appliance", name="test", node="backend")
42+
assert result.status == "CHANGED"
43+
assert result.data["changed"] == True
44+
45+
# Check that node attr changed
46+
result = host.run("stack list appliance attr test attr=node output-format=json")
47+
assert result.rc == 0
48+
assert json.loads(result.stdout) == [
49+
{
50+
"appliance": "test",
51+
"scope": "appliance",
52+
"type": "var",
53+
"attr": "node",
54+
"value": "backend"
55+
}
56+
]
57+
58+
# Test idempotency by adding again
59+
result = run_ansible_module("stacki_appliance", name="test", node="backend")
60+
assert result.status == "SUCCESS"
61+
assert result.data["changed"] == False
62+
63+
64+
def test_modify_appliance_with_node(self, host, run_ansible_module):
65+
# Add the appliance with a node parameter
66+
result = run_ansible_module("stacki_appliance", name="test", node="test")
67+
68+
assert result.status == "CHANGED"
69+
assert result.data["changed"] == True
70+
71+
# Confirm that the node attr is set for the appliance
72+
result = host.run("stack list appliance attr test attr=node output-format=json")
73+
assert result.rc == 0
74+
assert json.loads(result.stdout) == [
75+
{
76+
"appliance": "test",
77+
"scope": "appliance",
78+
"type": "var",
79+
"attr": "node",
80+
"value": "test"
81+
}
82+
]
83+
84+
# Now change the node
85+
result = run_ansible_module("stacki_appliance", name="test", node="backend")
86+
assert result.status == "CHANGED"
87+
assert result.data["changed"] == True
88+
89+
# Check that node attr changed
90+
result = host.run("stack list appliance attr test attr=node output-format=json")
91+
assert result.rc == 0
92+
assert json.loads(result.stdout) == [
93+
{
94+
"appliance": "test",
95+
"scope": "appliance",
96+
"type": "var",
97+
"attr": "node",
98+
"value": "backend"
99+
}
100+
]
101+
102+
# Test idempotency by adding again
103+
result = run_ansible_module("stacki_appliance", name="test", node="backend")
104+
assert result.status == "SUCCESS"
105+
assert result.data["changed"] == False
106+
107+
def test_remove_appliance(self, add_appliance, host, run_ansible_module):
108+
# Remove the appliance
109+
result = run_ansible_module("stacki_appliance", name="test", state="absent")
110+
assert result.status == "CHANGED"
111+
assert result.data["changed"] == True
112+
113+
# And confirm it is gone
114+
result = host.run("stack list appliance test")
115+
assert result.rc == 255
116+
assert "not a valid appliance" in result.stderr
117+
118+
# Test idempotency by removing it again
119+
result = run_ansible_module("stacki_appliance", name="test", state="absent")
120+
assert result.status == "SUCCESS"
121+
assert result.data["changed"] == False
122+
123+
def test_bad_name(self, run_ansible_module):
124+
result = run_ansible_module("stacki_appliance", name="%", state="absent")
125+
126+
assert result.status == "FAILED!"
127+
assert result.data["changed"] == False
128+
129+
assert "error" in result.data["msg"]
130+
assert "more than one appliance matches name" in result.data["msg"]

0 commit comments

Comments
 (0)