Skip to content

Commit aaeed2d

Browse files
author
Volodymyr Rudniev
committed
Add module for power management of dedicated servers
1 parent 4d301dd commit aaeed2d

File tree

5 files changed

+422
-1
lines changed

5 files changed

+422
-1
lines changed

ansible_collections/serverscom/sc_api/galaxy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
namespace: serverscom
33
name: sc_api
4-
version: 0.1.2
4+
version: 0.2.0
55
readme: README.md
66
authors:
77
- George Shuklin <george.shuklin@gmail.com>

ansible_collections/serverscom/sc_api/plugins/module_utils/modules.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,3 +1845,91 @@ def run(self):
18451845
return new_instance
18461846
else:
18471847
return {"changed": False}
1848+
1849+
1850+
class ScDedicatedServerPower:
1851+
def __init__(self, endpoint, token, server_id, state, fail_on_absent, timeout, checkmode):
1852+
self.api = ScApi(token, endpoint)
1853+
self.server_id = server_id
1854+
self.state = state
1855+
self.fail_on_absent = fail_on_absent
1856+
self.timeout = timeout
1857+
self.checkmode = checkmode
1858+
self.interval = 5
1859+
1860+
def wait_for_status(self, target_status):
1861+
start = time.time()
1862+
while True:
1863+
server = self.api.get_dedicated_servers(self.server_id)
1864+
status = server["power_status"]
1865+
if status == target_status:
1866+
return server
1867+
# only these are transitional
1868+
if status not in ("powering_on", "powering_off", "power_cycling"):
1869+
raise ModuleError(f"Unexpected power_status={status}, expected {target_status}")
1870+
if time.time() - start > self.timeout:
1871+
raise ModuleError(f"Timeout waiting for power_status={target_status}, last={status}")
1872+
time.sleep(self.interval)
1873+
1874+
def power_on(self):
1875+
try:
1876+
server = self.api.get_dedicated_servers(self.server_id)
1877+
except APIError404:
1878+
if self.fail_on_absent:
1879+
raise
1880+
raise ModuleError(f"Server {self.server_id} not found.")
1881+
if server["power_status"] == "powered_on":
1882+
server["changed"] = False
1883+
return server
1884+
if self.checkmode:
1885+
server["changed"] = True
1886+
return server
1887+
self.api.post_dedicated_server_power_on(self.server_id)
1888+
server = self.wait_for_status("powered_on")
1889+
server["changed"] = True
1890+
return server
1891+
1892+
def power_off(self):
1893+
try:
1894+
server = self.api.get_dedicated_servers(self.server_id)
1895+
except APIError404:
1896+
if self.fail_on_absent:
1897+
raise
1898+
raise ModuleError(f"Server {self.server_id} not found.")
1899+
if server["power_status"] == "powered_off":
1900+
server["changed"] = False
1901+
return server
1902+
if self.checkmode:
1903+
server["changed"] = True
1904+
return server
1905+
self.api.post_dedicated_server_power_off(self.server_id)
1906+
server = self.wait_for_status("powered_off")
1907+
server["changed"] = True
1908+
return server
1909+
1910+
def power_cycle(self):
1911+
try:
1912+
server = self.api.get_dedicated_servers(self.server_id)
1913+
except APIError404:
1914+
if self.fail_on_absent:
1915+
raise
1916+
raise ModuleError(f"Server {self.server_id} not found.")
1917+
if self.checkmode:
1918+
server["changed"] = True
1919+
return server
1920+
self.api.post_dedicated_server_power_off(self.server_id)
1921+
self.wait_for_status("powered_off")
1922+
self.api.post_dedicated_server_power_on(self.server_id)
1923+
server = self.wait_for_status("powered_on")
1924+
server["changed"] = True
1925+
return server
1926+
1927+
def run(self):
1928+
if self.state == "on":
1929+
return self.power_on()
1930+
elif self.state == "off":
1931+
return self.power_off()
1932+
elif self.state == "cycle":
1933+
return self.power_cycle()
1934+
else:
1935+
raise ModuleError(f"Unknown state: {self.state}")

ansible_collections/serverscom/sc_api/plugins/module_utils/sc_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,3 +782,19 @@ def lb_instance_l7_update(
782782
query_parameters=None,
783783
good_codes=[200, 202],
784784
)
785+
786+
def post_dedicated_server_power_on(self, server_id):
787+
return self.api_helper.make_post_request(
788+
path=f"/hosts/dedicated_servers/{server_id}/power_on",
789+
body=None,
790+
query_parameters=None,
791+
good_codes=[202],
792+
)
793+
794+
def post_dedicated_server_power_off(self, server_id):
795+
return self.api_helper.make_post_request(
796+
path=f"/hosts/dedicated_servers/{server_id}/power_off",
797+
body=None,
798+
query_parameters=None,
799+
good_codes=[202],
800+
)
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# (c) 2020, Servers.com
4+
# GNU General Public License v3.0
5+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
8+
from __future__ import absolute_import, division, print_function
9+
10+
11+
__metaclass__ = type
12+
13+
14+
ANSIBLE_METADATA = {
15+
"metadata_version": "1.1",
16+
"status": ["preview"],
17+
"supported_by": "community",
18+
}
19+
20+
DOCUMENTATION = """
21+
---
22+
module: sc_dedicated_server_power
23+
version_added: "1.0.0"
24+
author: "Volodymyr Rudniev (@koef)"
25+
short_description: Power on/off Bare Metal server
26+
description: >
27+
Manage the power state of a Bare Metal server outlet. Power on will start
28+
the server only if OOB configured accordingly.
29+
30+
options:
31+
endpoint:
32+
type: str
33+
default: "https://api.servers.com/v1"
34+
description:
35+
- API endpoint to connect to.
36+
37+
token:
38+
type: str
39+
required: true
40+
description:
41+
- API bearer token.
42+
- Obtain from https://portal.servers.com under Profile → Public API.
43+
44+
server_id:
45+
type: str
46+
required: true
47+
description:
48+
- ID of the Bare Metal server.
49+
- Use I(serverscom.sc_api.sc_baremetal_servers_info) to retrieve servers.
50+
51+
state:
52+
type: str
53+
required: true
54+
choices: ['on', 'off', 'cycle']
55+
description:
56+
- Desired power state.
57+
- "I(on): power on outlet."
58+
- "I(off): power off outlet."
59+
- "I(cycle): power cycle outlet."
60+
61+
fail_on_absent:
62+
type: bool
63+
default: true
64+
description:
65+
- Raise error if server is not found.
66+
- If set to false, absent (not found) server will have
67+
found=false in server info.
68+
69+
timeout:
70+
description:
71+
- |
72+
Maximum time in seconds to wait for the server to reach the desired state."
73+
required: false
74+
type: int
75+
default: 60
76+
"""
77+
78+
RETURN = """
79+
id:
80+
description: Unique identifier of a server.
81+
type: str
82+
returned: on success
83+
84+
title:
85+
description: Displayed name of the server (defaults to hostname).
86+
type: str
87+
returned: on success
88+
89+
type:
90+
description: "Resource type (always 'dedicated_server')."
91+
type: str
92+
returned: on success
93+
94+
rack_id:
95+
description: Unique identifier of the rack, or null if provisioning.
96+
type: str
97+
returned: on success
98+
99+
status:
100+
description: Provisioning state of the server (init, pending, active).
101+
type: str
102+
returned: on success
103+
104+
operational_status:
105+
description: Detailed operational state (normal, provisioning, installation, entering_rescue_mode, rescue_mode, exiting_rescue_mode).
106+
type: str
107+
returned: on success
108+
109+
power_status:
110+
description: Power state indicator (unknown, powering_on, powered_on, powering_off, powered_off, power_cycling).
111+
type: str
112+
returned: on success
113+
114+
configuration:
115+
description: Chassis model, RAM, and disk details.
116+
type: str
117+
returned: on success
118+
119+
location_id:
120+
description: Numeric identifier of the server's location.
121+
type: int
122+
returned: on success
123+
124+
location_code:
125+
description: Technical code of the server's location.
126+
type: str
127+
returned: on success
128+
129+
private_ipv4_address:
130+
description: Private IPv4 address, or null if unassigned.
131+
type: str
132+
returned: on success
133+
134+
public_ipv4_address:
135+
description: Public IPv4 address, or null if unassigned.
136+
type: str
137+
returned: on success
138+
139+
lease_start_at:
140+
description: Date when leasing began, or null.
141+
type: str
142+
returned: on success
143+
144+
scheduled_release_at:
145+
description: Scheduled release date-time, or null.
146+
type: str
147+
returned: on success
148+
149+
configuration_details:
150+
description: Detailed configuration object.
151+
type: dict
152+
returned: on success
153+
154+
labels:
155+
description: Labels associated with the server resource.
156+
type: dict
157+
returned: on success
158+
159+
created_at:
160+
description: Timestamp when the server was created.
161+
type: str
162+
returned: on success
163+
164+
updated_at:
165+
description: Timestamp of the last update.
166+
type: str
167+
returned: on success
168+
169+
oob_ipv4_address:
170+
description: Out-of-band IPv4 address if OOB access is enabled, or null.
171+
type: str
172+
returned: on success
173+
"""
174+
175+
EXAMPLES = """
176+
- name: Power on server
177+
sc_dedicated_server_power:
178+
token: "{{ api_token }}"
179+
server_id: "0m592Zmn"
180+
state: on
181+
182+
- name: Power off server
183+
sc_dedicated_server_power:
184+
token: "{{ api_token }}"
185+
server_id: "0m592Zmn"
186+
state: off
187+
188+
- name: Cycle power on server
189+
sc_dedicated_server_power:
190+
token: "{{ api_token }}"
191+
server_id: "0m592Zmn"
192+
state: cycle
193+
"""
194+
195+
196+
from ansible.module_utils.basic import AnsibleModule
197+
from ansible_collections.serverscom.sc_api.plugins.module_utils.modules import (
198+
DEFAULT_API_ENDPOINT,
199+
SCBaseError,
200+
ScDedicatedServerPower,
201+
)
202+
203+
__metaclass__ = type
204+
205+
206+
def main():
207+
module = AnsibleModule(
208+
argument_spec={
209+
"endpoint": {"default": DEFAULT_API_ENDPOINT},
210+
"token": {"type": "str", "no_log": True, "required": True},
211+
"server_id": {"type": "str", "required": True},
212+
"state": {
213+
"type": "str",
214+
"choices": ["on", "off", "cycle"],
215+
"required": True,
216+
},
217+
"fail_on_absent": {"type": "bool", "default": True},
218+
"timeout": {"type": "int", "default": 60},
219+
},
220+
supports_check_mode=True,
221+
)
222+
223+
try:
224+
power = ScDedicatedServerPower(
225+
endpoint=module.params["endpoint"],
226+
token=module.params["token"],
227+
server_id=module.params["server_id"],
228+
state=module.params["state"],
229+
fail_on_absent=module.params["fail_on_absent"],
230+
timeout=module.params["timeout"],
231+
checkmode=module.check_mode,
232+
)
233+
module.exit_json(**power.run())
234+
except SCBaseError as e:
235+
module.exit_json(**e.fail())
236+
237+
238+
if __name__ == "__main__":
239+
main()

0 commit comments

Comments
 (0)