Skip to content

Commit 83013ad

Browse files
committed
Add free space check before snapshot creation and update README
1 parent 8f67628 commit 83013ad

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ chmod +x /root/proxmox-autosnap/proxmox-autosnap.py
3434
| tags | no | list | empty | Space separated list of tags |
3535
| exclude-tags | no | list | empty | Space separated list of tags to exclude |
3636
| force | no | bool | false | Force removal from the config, even if snapshot deletion fails. |
37+
| check-free-space | no | bool | false | Verify sufficient storage space before snapshot creation. |
3738

3839
> proxmox-autosnap.py --help
3940

proxmox-autosnap.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
DATE_HUMAN_FORMAT = False
1818
DATE_TRUENAS_FORMAT = False
1919
INCLUDE_VM_STATE = False
20+
CHECK_FREE_SPACE = False
2021

2122
# Name of the currently running node
2223
NODE_NAME = socket.gethostname().split('.')[0]
@@ -112,6 +113,49 @@ def get_zfs_volume(proxmox_fs: str, virtualization: str) -> str:
112113
return zfsvol
113114

114115

116+
def fetch_storage_details() -> dict:
117+
if not CHECK_FREE_SPACE:
118+
return {}
119+
120+
cmd = [
121+
'pvesh', 'get',
122+
'/nodes/{0}/storage'.format(NODE_NAME),
123+
'--content', 'rootdir',
124+
'--enabled', '1',
125+
'--output-format', 'json',
126+
]
127+
run = run_command(cmd)
128+
if not run['status']:
129+
raise SystemExit(run['message'])
130+
131+
try:
132+
storages = json.loads(run['message'])
133+
except json.JSONDecodeError as e:
134+
raise SystemExit('Error decoding JSON: {0}'.format(e))
135+
136+
result = {}
137+
for storage in storages:
138+
storage_name = storage.get('storage')
139+
cmd = [
140+
'pvesh', 'get',
141+
'/nodes/{0}/storage/{1}/content'.format(NODE_NAME, storage_name),
142+
'--output-format', 'json'
143+
]
144+
run = run_command(cmd)
145+
if not run['status']:
146+
raise SystemExit(run['message'])
147+
148+
try:
149+
content = json.loads(run['message'])
150+
except json.JSONDecodeError as e:
151+
raise SystemExit('Error decoding JSON: {0}'.format(e))
152+
153+
vmids = sorted({int(item['vmid']) for item in content if 'vmid' in item})
154+
result[storage_name] = {'used_fraction': round(storage.get('used_fraction'), 2), 'vmids': vmids}
155+
156+
return result
157+
158+
115159
def get_vmids(exclude: list) -> dict:
116160
run = run_command(['cat', '/etc/pve/.vmlist'])
117161
if not run['status']:
@@ -124,6 +168,7 @@ def get_vmids(exclude: list) -> dict:
124168

125169
# Capture non-excluded, local VMs by type (vm vs container)
126170
result = {}
171+
storages = fetch_storage_details()
127172
for vmid, vm in json_data['ids'].items():
128173
if vm['node'] == NODE_NAME and vmid not in exclude:
129174
if vm['type'] == 'lxc':
@@ -139,6 +184,10 @@ def get_vmids(exclude: list) -> dict:
139184
if ONLY_ON_RUNNING and vm_is_stopped(vmid, virtualization):
140185
continue
141186

187+
if any(info['used_fraction'] >= 0.99 and int(vmid) in info['vmids'] for info in storages.values()):
188+
print(f'VM {vmid} - Storage usage is above 99%, skipping...') if not MUTE else None
189+
continue
190+
142191
result[vmid] = virtualization
143192

144193
return result
@@ -315,12 +364,14 @@ def main():
315364
parser.add_argument('--sudo', action='store_true', help='Launch commands through sudo.')
316365
parser.add_argument('--force', action='store_true',
317366
help='Force removal from config file, even if disk snapshot deletion fails.')
367+
parser.add_argument('--check-free-space', action='store_true',
368+
help='Check if there is enough free space on the storage before creating a snapshot.')
318369
argp = parser.parse_args()
319370

320371
if not argp.vmid and not argp.tags and not argp.exclude_tags:
321372
parser.error('At least one of --vmid or --tags or --exclude-tags is required.')
322373

323-
global MUTE, FORCE, DRY_RUN, USE_SUDO, ONLY_ON_RUNNING, INCLUDE_VM_STATE, DATE_ISO_FORMAT, DATE_HUMAN_FORMAT, DATE_TRUENAS_FORMAT
374+
global MUTE, FORCE, DRY_RUN, USE_SUDO, ONLY_ON_RUNNING, INCLUDE_VM_STATE, DATE_ISO_FORMAT, DATE_HUMAN_FORMAT, DATE_TRUENAS_FORMAT, CHECK_FREE_SPACE
324375
MUTE = argp.mute
325376
FORCE = argp.force
326377
DRY_RUN = argp.dryrun
@@ -330,6 +381,7 @@ def main():
330381
DATE_HUMAN_FORMAT = argp.date_human_format
331382
DATE_TRUENAS_FORMAT = argp.date_truenas_format
332383
INCLUDE_VM_STATE = argp.includevmstate
384+
CHECK_FREE_SPACE = argp.check_free_space
333385

334386
picked_vmid = get_filtered_vmids(vmids=argp.vmid, exclude=argp.exclude, tags=argp.tags,
335387
exclude_tags=argp.exclude_tags)

0 commit comments

Comments
 (0)