Skip to content

Commit 90b64aa

Browse files
authored
Merge pull request #36 from serverscom/rbs-volumes
Add RBS modules
2 parents 35b1ed6 + 63777f6 commit 90b64aa

File tree

12 files changed

+1592
-8
lines changed

12 files changed

+1592
-8
lines changed

.github/workflows/tests.yaml

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ jobs:
4848
integration_tests_cloud:
4949
runs-on: ubuntu-24.04
5050
needs: quick_tests
51-
concurrency: API
51+
concurrency:
52+
group: cloud
53+
cancel-in-progress: false
5254
steps:
5355
- name: Checkout
5456
uses: actions/checkout@v4
@@ -89,8 +91,10 @@ jobs:
8991
9092
integration_tests_bm:
9193
runs-on: ubuntu-24.04
92-
needs: integration_tests_cloud
93-
concurrency: API
94+
needs: quick_tests
95+
concurrency:
96+
group: BM
97+
cancel-in-progress: false
9498
steps:
9599
- name: Checkout
96100
uses: actions/checkout@v4
@@ -129,7 +133,9 @@ jobs:
129133
integration_tests_l2:
130134
runs-on: ubuntu-24.04
131135
needs: integration_tests_bm
132-
concurrency: API
136+
concurrency:
137+
group: BM
138+
cancel-in-progress: false
133139
steps:
134140
- name: Checkout
135141
uses: actions/checkout@v4
@@ -162,8 +168,10 @@ jobs:
162168
163169
integration_tests_lb_instances:
164170
runs-on: ubuntu-24.04
165-
needs: integration_tests_bm
166-
concurrency: API
171+
needs: quick_tests
172+
concurrency:
173+
group: LB
174+
cancel-in-progress: false
167175
steps:
168176
- name: Checkout
169177
uses: actions/checkout@v4
@@ -195,11 +203,51 @@ jobs:
195203
sync
196204
sync
197205
206+
integration_tests_rbs_volumes:
207+
runs-on: ubuntu-24.04
208+
needs: quick_tests
209+
concurrency:
210+
group: RBS
211+
cancel-in-progress: false
212+
steps:
213+
- name: Checkout
214+
uses: actions/checkout@v4
215+
- name: Set up Python
216+
uses: actions/setup-python@v5
217+
with:
218+
python-version: "3.13"
219+
- name: Install dependencies
220+
run: |
221+
python -m pip install --upgrade pip
222+
pip install -r requirements.txt
223+
- name: Configure integration tests
224+
run: |
225+
envsubst < integration_config.yml.template > ansible_collections/serverscom/sc_api/tests/integration/integration_config.yml
226+
env:
227+
SC_TOKEN: "${{ secrets.SC_TOKEN }}"
228+
- name: Integration tests
229+
run: ansible-test integration --requirements --python 3.13
230+
sc_rbs_flavors_info
231+
sc_rbs_volume
232+
sc_rbs_volume_info
233+
sc_rbs_volume_credentials_reset
234+
235+
working-directory: ansible_collections/serverscom/sc_api
236+
- name: Cleanup secrets
237+
if: always()
238+
run: |
239+
dd if=/dev/zero bs=4k count=4 of=ansible_collections/serverscom/sc_api/tests/integration/integration_config.yml
240+
sync
241+
sync
242+
198243
build:
199244
runs-on: ubuntu-24.04
200245
needs:
246+
- integration_tests_bm
247+
- integration_tests_cloud
201248
- integration_tests_l2
202249
- integration_tests_lb_instances
250+
- integration_tests_rbs_volumes
203251
outputs:
204252
version: ${{ steps.version.outputs.version }}
205253
steps:

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.3.1
4+
version: 0.4.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: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,3 +2156,284 @@ def run(self):
21562156
raise ModuleError("No operating systems found matching the criteria")
21572157

21582158
return {"changed": False, "os_list": os_list}
2159+
2160+
2161+
class ScRBSFlavorsInfo:
2162+
def __init__(self, endpoint, token, location_id):
2163+
self.api = ScApi(token, endpoint)
2164+
self.location_id = location_id
2165+
2166+
def run(self):
2167+
return {
2168+
"changed": False,
2169+
"rbs_volume_flavors": list(self.api.list_rbs_flavors(self.location_id)),
2170+
}
2171+
2172+
2173+
class ScRBSVolumeList:
2174+
def __init__(self, endpoint, token, label_selector=None, search_pattern=None, location_id=None, location_code=None):
2175+
self.api = ScApi(token, endpoint)
2176+
self.label_selector = label_selector
2177+
self.search_pattern = search_pattern
2178+
self.location_id = location_id
2179+
self.location_code = location_code
2180+
2181+
if not self.location_id and self.location_code:
2182+
self.location_code = self.location_code.upper()
2183+
locations = self.api.list_locations(search_pattern=self.location_code)
2184+
2185+
for location in locations:
2186+
if location.get("code") == self.location_code:
2187+
self.location_id = location.get("id")
2188+
break
2189+
2190+
if not self.location_id:
2191+
raise ModuleError(
2192+
f"Location with code '{self.location_code}' not found."
2193+
)
2194+
2195+
def run(self):
2196+
volumes = list(self.api.list_rbs_volumes(self.label_selector, self.search_pattern, self.location_id))
2197+
for volume in volumes:
2198+
volume_credentials = self.api.get_rbs_volume_credentials(volume["id"])
2199+
volume.update({"username": volume_credentials["username"], "password": volume_credentials["password"]})
2200+
return {
2201+
"changed": False,
2202+
"rbs_volumes": volumes,
2203+
}
2204+
2205+
2206+
class scRBSVolumeCreateUpdateDelete:
2207+
def __init__(
2208+
self,
2209+
endpoint,
2210+
token,
2211+
name,
2212+
location_id,
2213+
location_code,
2214+
flavor_id,
2215+
flavor_name,
2216+
size,
2217+
labels,
2218+
volume_id,
2219+
wait,
2220+
update_interval,
2221+
checkmode
2222+
):
2223+
self.api = ScApi(token, endpoint)
2224+
self.volume_id = volume_id
2225+
self.name = name
2226+
self.location_id = location_id
2227+
self.location_code = location_code
2228+
self.flavor_id = flavor_id
2229+
self.flavor_name = flavor_name
2230+
self.size = size
2231+
self.labels = labels
2232+
self.wait = wait
2233+
self.update_interval = update_interval
2234+
self.checkmode = checkmode
2235+
2236+
if not self.location_id and self.location_code:
2237+
self.location_code = self.location_code.upper()
2238+
locations = self.api.list_locations(search_pattern=self.location_code)
2239+
2240+
for location in locations:
2241+
if location.get("code") == self.location_code:
2242+
self.location_id = location.get("id")
2243+
break
2244+
2245+
if not self.location_id:
2246+
raise ModuleError(
2247+
f"Location with code '{self.location_code}' not found."
2248+
)
2249+
2250+
if not self.flavor_id and self.flavor_name:
2251+
flavors = self.api.list_rbs_flavors(location_id=self.location_id)
2252+
2253+
for flavor in flavors:
2254+
if flavor.get("name") == self.flavor_name:
2255+
self.flavor_id = flavor.get("id")
2256+
break
2257+
2258+
if not self.flavor_id:
2259+
raise ModuleError(
2260+
f"Flavor with the name '{self.flavor_name}' not found."
2261+
)
2262+
2263+
def update_volume(self):
2264+
volume = self.api.get_rbs_volume(self.volume_id)
2265+
result = {"changed": False, "rbs_volume": volume}
2266+
if any([
2267+
self.name and self.name != volume.get("name"),
2268+
self.size and self.size != volume.get("size"),
2269+
self.labels and self.labels != volume.get("labels"),
2270+
]):
2271+
if not self.checkmode:
2272+
updated_volume = self.api.update_rbs_volume(
2273+
self.volume_id,
2274+
name=self.name,
2275+
size=self.size,
2276+
labels=self.labels,
2277+
)
2278+
updated_volume = self.wait_for_active()
2279+
result["rbs_volume"] = updated_volume
2280+
else:
2281+
result["rbs_volume"]["name"] = self.name or volume.get("name")
2282+
result["rbs_volume"]["size"] = self.size or volume.get("size")
2283+
result["rbs_volume"]["labels"] = self.labels or volume.get("labels")
2284+
result["changed"] = True
2285+
return result
2286+
else:
2287+
result["changed"] = False
2288+
return result
2289+
2290+
def create_or_update_volume(self):
2291+
result = {"changed": False, "rbs_volume": None}
2292+
existing_volume = self.api.get_rbs_volume_by_name(self.name)
2293+
if existing_volume:
2294+
if (self.location_id and self.location_id != existing_volume["location_id"]) or (self.flavor_id and self.flavor_id != existing_volume["flavor_id"]):
2295+
raise ModuleError(f"RBS volume with name '{self.name}' already exists. You cannot change its location or flavor.")
2296+
else:
2297+
self.volume_id = existing_volume["id"]
2298+
upd = self.update_volume()
2299+
return upd
2300+
else:
2301+
if not (self.location_id and self.flavor_id and self.size):
2302+
raise ModuleError(f"RBS volume with name '{self.name}' does not exist. "
2303+
"To create it, location_id (or location_code), flavor_id (or flavor_name) and size must be provided.")
2304+
if not self.checkmode:
2305+
response = self.api.create_rbs_volume(
2306+
name=self.name,
2307+
location_id=self.location_id,
2308+
flavor_id=self.flavor_id,
2309+
size=self.size,
2310+
labels=self.labels,
2311+
)
2312+
self.volume_id = response.get("id")
2313+
new_volume = self.wait_for_active()
2314+
result["rbs_volume"] = new_volume
2315+
else:
2316+
result["rbs_volume"] = {"id": None,
2317+
"name": self.name,
2318+
"location_id": self.location_id,
2319+
"location_code": None,
2320+
"flavor_id": self.flavor_id,
2321+
"size": self.size,
2322+
"labels": self.labels,
2323+
"status": "creating"}
2324+
result["changed"] = True
2325+
return result
2326+
2327+
def delete_volume(self):
2328+
no_volume = False
2329+
if self.name and not self.volume_id:
2330+
volume = self.api.get_rbs_volume_by_name(self.name)
2331+
if volume:
2332+
self.volume_id = volume["id"]
2333+
else:
2334+
no_volume = True
2335+
try:
2336+
volume = self.api.get_rbs_volume(self.volume_id)
2337+
except APIError404:
2338+
no_volume = True
2339+
if not self.checkmode and not no_volume:
2340+
if volume["status"] != "removing":
2341+
self.api.delete_rbs_volume(self.volume_id)
2342+
self.wait_for_disappearance()
2343+
return {"changed": not no_volume, "rbs_volume": {}}
2344+
2345+
def wait_for_active(self):
2346+
volume = self.api.get_rbs_volume(self.volume_id)
2347+
if self.wait == 0:
2348+
return volume
2349+
start_time = time.time()
2350+
while True:
2351+
if volume["status"] == "active":
2352+
return volume
2353+
elapsed = time.time() - start_time
2354+
if elapsed > self.wait:
2355+
raise WaitError(
2356+
msg=f"Timeout waiting for RBS volume {self.volume_id} to become active after {elapsed:.2f} seconds.",
2357+
timeout=elapsed,
2358+
)
2359+
time.sleep(self.update_interval)
2360+
volume = self.api.get_rbs_volume(self.volume_id)
2361+
2362+
def wait_for_disappearance(self):
2363+
if self.wait == 0:
2364+
return
2365+
start_time = time.time()
2366+
while True:
2367+
try:
2368+
self.api.get_rbs_volume(self.volume_id)
2369+
except APIError404:
2370+
return []
2371+
elapsed = time.time() - start_time
2372+
if elapsed > self.wait:
2373+
raise WaitError(
2374+
msg=f"Timeout waiting for RBS volume {self.volume_id} to disappear after {elapsed:.2f} seconds.",
2375+
timeout=elapsed,
2376+
)
2377+
time.sleep(self.update_interval)
2378+
2379+
2380+
class ScRBSVolumeCredentialsInfo:
2381+
def __init__(self, endpoint, token, volume_id, name):
2382+
self.api = ScApi(token, endpoint)
2383+
self.volume_id = volume_id
2384+
self.name = name
2385+
2386+
def run(self):
2387+
if self.name and not self.volume_id:
2388+
existing_volume = self.api.get_rbs_volume_by_name(self.name)
2389+
if existing_volume:
2390+
self.volume_id = existing_volume["id"]
2391+
else:
2392+
raise ModuleError(f"RBS volume with name '{self.name}' not found.")
2393+
rbs_volume_credentials = self.api.get_rbs_volume_credentials(self.volume_id)
2394+
return {
2395+
"changed": False,
2396+
"rbs_volume_credentials": rbs_volume_credentials,
2397+
}
2398+
2399+
2400+
class ScRBSVolumeCredentialsReset:
2401+
def __init__(self, endpoint, token, wait, update_interval, checkmode, volume_id, name):
2402+
self.api = ScApi(token, endpoint)
2403+
self.volume_id = volume_id
2404+
self.name = name
2405+
self.wait = wait
2406+
self.update_interval = update_interval
2407+
self.checkmode = checkmode
2408+
2409+
def run(self):
2410+
if self.name and not self.volume_id:
2411+
existing_volume = self.api.get_rbs_volume_by_name(self.name)
2412+
if existing_volume:
2413+
self.volume_id = existing_volume["id"]
2414+
else:
2415+
raise ModuleError(f"RBS volume with name '{self.name}' not found.")
2416+
if not self.checkmode:
2417+
rbs_volume = self.api.reset_rbs_volume_credentials(self.volume_id)
2418+
if self.wait > 0:
2419+
start_time = time.time()
2420+
while True:
2421+
if rbs_volume["status"] == "active":
2422+
break
2423+
elapsed = time.time() - start_time
2424+
if elapsed > self.wait:
2425+
raise WaitError(
2426+
msg=f"Timeout waiting for RBS volume {self.volume_id} to become active after {elapsed:.2f} seconds.",
2427+
timeout=elapsed,
2428+
)
2429+
time.sleep(self.update_interval)
2430+
rbs_volume = self.api.get_rbs_volume(self.volume_id)
2431+
rbs_volume_credentials = self.api.get_rbs_volume_credentials(self.volume_id)
2432+
return {
2433+
"changed": True,
2434+
"rbs_volume_credentials": rbs_volume_credentials,
2435+
}
2436+
return {
2437+
"changed": True,
2438+
"rbs_volume_credentials": {},
2439+
}

0 commit comments

Comments
 (0)