Skip to content

Commit 93bb760

Browse files
committed
feat(docs): add cline thinking notes
1 parent ee8fb96 commit 93bb760

File tree

2 files changed

+192
-3
lines changed

2 files changed

+192
-3
lines changed

notes/2025-03-22-cline-thinking-en.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: Debugging a Hetzner Cloud Server Snapshot Script
3+
lang: en
4+
layout: post
5+
audio: false
6+
translated: false
7+
generated: true
8+
---
9+
10+
# Debugging and Enhancing a Hetzner Cloud Server Snapshot Script
11+
12+
This article documents the process of troubleshooting and improving a Python script for managing Hetzner Cloud server snapshots and cloning servers.
13+
14+
## The Initial Problem
15+
16+
The script was encountering an error where a 'Server' object had no attribute 'create_snapshot'. The issue was in the `scripts/auto-ss-config/hetzner.py` file, which needed to be fixed to properly create snapshots.
17+
18+
## Step 1: Identifying the Bug
19+
20+
After examining the code, I identified that the error occurred because the script was trying to call `create_snapshot()` directly on a server object, but this method doesn't exist in the hcloud Python library's Server class.
21+
22+
The initial problematic code was:
23+
24+
```python
25+
snapshot = server.create_snapshot(name=f"{server.name}-snapshot")
26+
```
27+
28+
## Step 2: First Fix Attempt
29+
30+
The first fix was to use the client's servers API instead:
31+
32+
```python
33+
snapshot = client.servers.create_snapshot(server, description=f"{server.name}-snapshot")
34+
```
35+
36+
However, this led to a new error: `'ServersClient' object has no attribute 'create_snapshot'`
37+
38+
## Step 3: Investigating the Correct API
39+
40+
To understand the correct API method, I examined the hcloud Python library. This revealed that I needed to use `client.servers.create_image()` rather than trying to use a non-existent method.
41+
42+
The correct solution was:
43+
44+
```python
45+
response = client.servers.create_image(server, description=f"{server.name}-snapshot", type="snapshot")
46+
```
47+
48+
## Step 4: Enhancing the Script
49+
50+
After fixing the initial bug, I enhanced the script to add the ability to create new servers from snapshots. This involved:
51+
52+
1. Waiting for snapshots to be fully available before using them
53+
2. Adding command-line arguments for flexibility
54+
3. Improving error handling
55+
4. Creating a more robust solution for checking snapshot status
56+
57+
## Final Implementation
58+
59+
The enhanced script now supports:
60+
61+
- Creating snapshots with: `python scripts/auto-ss-config/hetzner.py --create-snapshot [--server-name NAME]`
62+
- Creating a server from an existing snapshot: `python scripts/auto-ss-config/hetzner.py --create-server --snapshot-id ID [--server-name NAME]`
63+
- Doing both in one command: `python scripts/auto-ss-config/hetzner.py --create-snapshot --create-server [--server-name NAME]`
64+
65+
The script successfully creates snapshots and then uses them to create new server instances with the same specifications (server type, datacenter location) as the original server.
66+
67+
## Key Learnings
68+
69+
1. When working with cloud provider APIs, it's important to use the correct method calls as specified in their documentation
70+
2. Resources like snapshots often have states (e.g., "creating", "available") that need to be properly handled
71+
3. Adding command-line arguments improves script flexibility
72+
4. Proper error handling and waiting mechanisms are essential for robust cloud automation scripts
73+
74+
This debugging process demonstrates how to systematically identify and fix issues in Python scripts that interact with cloud provider APIs.

scripts/auto-ss-config/hetzner.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
from hcloud import Client
22
import os
3+
import time
4+
import argparse
5+
import sys
6+
7+
# Parse command line arguments
8+
parser = argparse.ArgumentParser(description='Hetzner Cloud server snapshot and clone tool')
9+
parser.add_argument('--create-snapshot', action='store_true', help='Create snapshots of servers')
10+
parser.add_argument('--create-server', action='store_true', help='Create servers from existing snapshots')
11+
parser.add_argument('--snapshot-id', type=str, help='Specific snapshot ID to use when creating a server')
12+
parser.add_argument('--server-name', type=str, help='Server name to operate on (optional, all servers if not specified)')
13+
args = parser.parse_args()
14+
15+
# If no action specified, show help and exit
16+
if not (args.create_snapshot or args.create_server):
17+
parser.print_help()
18+
sys.exit(0)
319

420
# Get the API token from the environment variable
521
api_token = os.environ.get('HERTZNER_API_KEY')
@@ -13,14 +29,90 @@
1329

1430
def create_snapshot(server):
1531
try:
32+
print(f"Creating snapshot for server {server.name}...")
33+
# Create the snapshot directly - the API will handle any locking issues
1634
response = client.servers.create_image(server, description=f"{server.name}-snapshot", type="snapshot")
1735
print(f"Snapshot created for server {server.name} with ID: {response.image.id}")
36+
return response.image
37+
except Exception as e:
38+
if "locked" in str(e).lower():
39+
print(f"Server {server.name} is locked. Please try again later.")
40+
else:
41+
print(f"Error creating snapshot for server {server.name}: {e}")
42+
return None
43+
44+
def create_server_from_snapshot(snapshot_id, source_server, new_name=None):
45+
try:
46+
if new_name is None:
47+
new_name = f"{source_server.name}-clone"
48+
49+
# Get the server type and location from the source server
50+
server_type = source_server.server_type
51+
datacenter = source_server.datacenter
52+
53+
print(f"Waiting for snapshot (ID: {snapshot_id}) to be available...")
54+
55+
# Wait for the snapshot to be available
56+
max_attempts = 20
57+
attempts = 0
58+
snapshot = None
59+
60+
while attempts < max_attempts:
61+
try:
62+
# Try to get the image
63+
snapshot = client.images.get_by_id(snapshot_id)
64+
print(f"Snapshot status: {snapshot.status}")
65+
66+
# Only proceed when the snapshot is actually available
67+
if snapshot.status == "available":
68+
print(f"Snapshot is now fully ready to use")
69+
break
70+
71+
print(f"Snapshot found but status is '{snapshot.status}', waiting for 'available' status...")
72+
except Exception as e:
73+
print(f"Attempt {attempts+1}/{max_attempts}: Snapshot not accessible yet: {e}")
74+
75+
# Increment attempts and wait regardless of error or not-ready status
76+
attempts += 1
77+
if attempts < max_attempts:
78+
sleep_time = 20 # Wait longer between attempts
79+
print(f"Waiting {sleep_time} seconds before retrying (attempt {attempts}/{max_attempts})...")
80+
time.sleep(sleep_time)
81+
82+
if snapshot is None or snapshot.status != "available":
83+
print("Failed to get a ready snapshot after multiple attempts")
84+
return None
85+
86+
print(f"Creating new server '{new_name}' from snapshot (ID: {snapshot_id})")
87+
print(f" - Using server type: {server_type.name}")
88+
print(f" - Using datacenter: {datacenter.name} (location: {datacenter.location.name})")
89+
90+
# Create the new server using the snapshot as the image
91+
response = client.servers.create(
92+
name=new_name,
93+
server_type=server_type,
94+
image=snapshot,
95+
datacenter=datacenter,
96+
start_after_create=True
97+
)
98+
99+
print(f"New server created with ID: {response.server.id}")
100+
print(f"Root password: {response.root_password}")
101+
return response.server
18102
except Exception as e:
19-
print(f"Error creating snapshot for server {server.name}: {e}")
103+
print(f"Error creating server from snapshot: {e}")
104+
return None
20105

21-
# List all servers
106+
# Get all servers
22107
servers = client.servers.get_all()
23108

109+
# Filter servers if server_name is specified
110+
if args.server_name:
111+
servers = [s for s in servers if s.name == args.server_name]
112+
if not servers:
113+
print(f"Error: No server found with name '{args.server_name}'")
114+
sys.exit(1)
115+
24116
# Print server details
25117
for server in servers:
26118
print(f"Server ID: {server.id}")
@@ -31,4 +123,27 @@ def create_snapshot(server):
31123
print(f"Server Type: {server.server_type.name}")
32124
print(f"Server Location: {server.datacenter.location.name}")
33125
print("----------------------------------")
34-
create_snapshot(server)
126+
127+
# Create snapshots if requested
128+
if args.create_snapshot:
129+
print("\n=== Creating Snapshots ===\n")
130+
for server in servers:
131+
snapshot = create_snapshot(server)
132+
if snapshot and args.create_server:
133+
# Create a new server from the snapshot if both flags are set
134+
new_server = create_server_from_snapshot(snapshot.id, server)
135+
136+
# Create servers from existing snapshot if requested
137+
if args.create_server and args.snapshot_id and not args.create_snapshot:
138+
print("\n=== Creating Server from Existing Snapshot ===\n")
139+
# Use the first server as template for the new server
140+
snapshot = None
141+
try:
142+
snapshot = client.images.get_by_id(args.snapshot_id)
143+
print(f"Found snapshot with ID {args.snapshot_id}")
144+
except Exception as e:
145+
print(f"Error: Could not find snapshot with ID {args.snapshot_id}: {e}")
146+
sys.exit(1)
147+
148+
if snapshot and servers:
149+
new_server = create_server_from_snapshot(args.snapshot_id, servers[0])

0 commit comments

Comments
 (0)