Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
lvm-snapshots
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Container registry
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
infra
lvm-snapshots
Commits
beec5311
Commit
beec5311
authored
8 years ago
by
Administrator
Browse files
Options
Downloads
Patches
Plain Diff
Better config options
parent
8c78f977
No related branches found
No related tags found
No related merge requests found
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
.gitignore
+1
-0
1 addition, 0 deletions
.gitignore
config.example.toml
+1
-0
1 addition, 0 deletions
config.example.toml
lvmsnapshot.py
+118
-70
118 additions, 70 deletions
lvmsnapshot.py
with
120 additions
and
70 deletions
.gitignore
+
1
−
0
View file @
beec5311
...
...
@@ -2,3 +2,4 @@ bin/
lib/
share/
local/
config.toml
This diff is collapsed.
Click to expand it.
config.example.toml
+
1
−
0
View file @
beec5311
...
...
@@ -2,6 +2,7 @@
[[volume]]
volume_group
=
"exports"
name
=
"pub"
path
=
".snapshots"
[[volume.keep]]
number
=
24
...
...
This diff is collapsed.
Click to expand it.
lvmsnapshot.py
+
118
−
70
View file @
beec5311
...
...
@@ -31,17 +31,56 @@ import json
from
contextlib
import
contextmanager
from
collections
import
OrderedDict
SNAPSHOT_BASE_DIR
=
"
/snapshots
"
TIMESTAMP_FORMAT
=
"
%Y-%m-%d-%H-%M
"
PERIOD_KEYS
=
OrderedDict
([
class
Config
:
snapshot_base_dir
=
"
/snapshots
"
timestamp_format
=
"
%Y-%m-%d-%H-%M
"
period_keys
=
OrderedDict
([
(
"
y
"
,
365
*
86400
),
(
"
m
"
,
31
*
86400
),
(
"
d
"
,
86400
),
(
"
H
"
,
3600
),
(
"
M
"
,
60
),
])
min_interval
=
None
volumes
=
{}
periods
=
{}
@staticmethod
def
load
():
import
toml
config_path
=
"
config.toml
"
global_config_path
=
"
/etc/lvm-snapshot.toml
"
ENV_VAR
=
"
LVM_SNAPSHOT_CONFIG
"
if
not
os
.
path
.
isfile
(
config_path
)
and
os
.
path
.
isfile
(
global_config_path
):
config_path
=
global_config_path
if
ENV_VAR
in
os
.
environ
:
config_path
=
os
.
environ
[
ENV_VAR
]
with
open
(
config_path
,
"
r
"
)
as
config_file
:
return
toml
.
load
(
config_file
)
@staticmethod
def
parse
(
config
):
periods
=
{}
min_interval
=
None
for
volume_conf
in
config
[
"
volume
"
]:
name
=
volume_conf
[
"
name
"
]
volume_group
=
volume_conf
[
"
volume_group
"
]
path
=
volume_conf
.
get
(
"
path
"
)
volume
=
Volume
(
volume_group
,
name
,
path
)
Config
.
volumes
[
volume
.
get_full_name
()]
=
volume
periods
[
volume
]
=
[]
for
raw_period
in
volume_conf
[
"
keep
"
]:
interval
=
Period
.
parse_interval
(
raw_period
[
"
interval
"
])
if
min_interval
is
None
or
interval
<
min_interval
:
min_interval
=
interval
number
=
int
(
raw_period
[
"
number
"
])
periods
[
volume
].
append
(
Period
(
target_number
=
number
,
interval
=
interval
))
Config
.
min_interval
=
min_interval
Config
.
periods
=
periods
def
run_process
(
command
,
check
=
True
):
logging
.
debug
(
"
running command {}
"
.
format
(
command
))
result
=
sp
.
run
(
command
,
check
=
check
,
stdout
=
sp
.
PIPE
,
stderr
=
sp
.
PIPE
)
if
result
.
stdout
:
logging
.
info
(
result
.
stdout
.
decode
(
"
utf-8
"
).
strip
())
...
...
@@ -64,9 +103,17 @@ def freeze_xfs(mountpoint, freeze):
run_process
(
command
,
check
=
True
)
class
Volume
:
def
__init__
(
self
,
volume_group
,
name
):
def
__init__
(
self
,
volume_group
,
name
,
snapshot_dir
=
None
):
self
.
volume_group
=
volume_group
self
.
name
=
name
if
snapshot_dir
is
None
:
if
self
.
get_full_name
()
in
Config
.
volumes
:
snapshot_dir
=
Config
.
volumes
[
self
.
get_full_name
()].
snapshot_dir
else
:
snapshot_dir
=
os
.
path
.
join
(
Config
.
snapshot_base_dir
,
name
)
if
not
os
.
path
.
isabs
(
snapshot_dir
):
snapshot_dir
=
os
.
path
.
join
(
self
.
get_mountpoint
(),
snapshot_dir
)
self
.
snapshot_dir
=
snapshot_dir
def
__repr__
(
self
):
return
self
.
get_full_name
()
...
...
@@ -85,6 +132,7 @@ class Volume:
def
get_mountpoint
(
self
):
mapper_device
=
self
.
get_mapper_device
()
logging
.
debug
(
"
searching volume mountpoint for {}
"
.
format
(
mapper_device
))
command
=
[
"
/bin/findmnt
"
,
"
--source
"
,
mapper_device
,
...
...
@@ -110,7 +158,7 @@ class Snapshot:
return
self
.
get_name
()
def
get_timestamp_str
(
self
):
return
self
.
timestamp
.
strftime
(
TIMESTAMP_FORMAT
)
return
self
.
timestamp
.
strftime
(
Config
.
timestamp_format
)
def
get_name
(
self
):
return
"
{}-snapshot-{}
"
.
format
(
...
...
@@ -120,7 +168,7 @@ class Snapshot:
@staticmethod
def
parse_name
(
name
):
parent_name
,
_
,
timestamp_str
=
name
.
split
(
"
-
"
,
2
)
timestamp
=
datetime
.
strptime
(
timestamp_str
,
TIMESTAMP_FORMAT
)
timestamp
=
datetime
.
strptime
(
timestamp_str
,
Config
.
timestamp_format
)
return
parent_name
,
timestamp
def
get_full_volume
(
self
):
...
...
@@ -128,12 +176,30 @@ class Snapshot:
def
get_mountpoint
(
self
):
return
os
.
path
.
join
(
SNAPSHOT_BASE_DIR
,
self
.
parent_volume
.
na
me
,
self
.
get_timestamp_str
()
self
.
parent_volume
.
s
na
pshot_dir
,
self
.
get_timestamp_str
()
)
def
create_mountpoint
(
self
):
logging
.
debug
(
"
creating mountpoint {}
"
.
format
(
self
.
get_mountpoint
()))
os
.
makedirs
(
self
.
get_mountpoint
(),
mode
=
0o755
,
exist_ok
=
True
)
def
find_mountpoint
(
self
):
logging
.
debug
(
"
searching snapshot mountpoint for {}
"
.
format
(
self
.
get_name
()))
command
=
[
"
/bin/findmnt
"
,
"
--source
"
,
self
.
get_mapper_device
(),
"
--json
"
,
]
try
:
result
=
sp
.
check_output
(
command
).
decode
(
"
utf-8
"
)
data
=
json
.
loads
(
result
)
filesystems
=
data
[
"
filesystems
"
]
if
len
(
filesystems
)
<
1
:
return
None
return
filesystems
[
0
][
"
target
"
]
except
sp
.
CalledProcessError
:
return
None
def
get_mapper_device
(
self
):
return
"
/dev/mapper/{}-{}
"
.
format
(
self
.
parent_volume
.
volume_group
,
...
...
@@ -149,6 +215,7 @@ class Snapshot:
self
.
get_mapper_device
())
def
create
(
self
):
logging
.
debug
(
"
creating snapshot {}
"
.
format
(
self
.
get_name
()))
parent_mountpoint
=
self
.
parent_volume
.
get_mountpoint
()
#with xfs_freeze(parent_mountpoint): # not necessary, lvm does this
create_command
=
[
...
...
@@ -162,7 +229,9 @@ class Snapshot:
self
.
mount
()
def
mount
(
self
):
logging
.
debug
(
"
mounting snapshot {}
"
.
format
(
self
.
get_name
()))
if
self
.
check_mount
():
logging
.
debug
(
"
already mounted
"
)
return
activate_command
=
[
"
/sbin/lvchange
"
,
...
...
@@ -193,6 +262,7 @@ class Snapshot:
return
False
def
remove
(
self
):
logging
.
debug
(
"
removing snapshot {}
"
.
format
(
self
.
get_name
()))
self
.
unmount
()
remove_command
=
[
"
/sbin/lvremove
"
,
...
...
@@ -201,12 +271,17 @@ class Snapshot:
run_process
(
remove_command
,
check
=
True
)
def
unmount
(
self
):
logging
.
debug
(
"
unmounting snapshot {}
"
.
format
(
self
.
get_name
()))
mountpoint
=
self
.
find_mountpoint
()
if
mountpoint
is
not
None
:
if
self
.
check_mount
():
unmount_command
=
[
"
/bin/umount
"
,
self
.
get_
mountpoint
()
mountpoint
]
run_process
(
unmount_command
,
check
=
True
)
if
os
.
path
.
isdir
(
mountpoint
):
os
.
rmdir
(
mountpoint
)
deactivate_command
=
[
"
/sbin/lvchange
"
,
"
--activate
"
,
"
n
"
,
...
...
@@ -215,7 +290,6 @@ class Snapshot:
]
run_process
(
deactivate_command
,
check
=
True
)
self
.
active
=
False
os
.
rmdir
(
self
.
get_mountpoint
())
@staticmethod
def
list_snapshots
():
...
...
@@ -272,17 +346,14 @@ class Period:
return
datetime
.
now
()
-
self
.
target_number
*
self
.
interval
@staticmethod
def
build_regex
(
period_keys
=
None
):
if
period_keys
is
None
:
period_keys
=
PERIOD_KEYS
parts
=
[
r
"
(?:(?P<{0}>\d+){0})?
"
.
format
(
key
)
for
key
in
period_keys
]
def
build_regex
():
parts
=
[
r
"
(?:(?P<{0}>\d+){0})?
"
.
format
(
key
)
for
key
in
Config
.
period_keys
]
return
""
.
join
(
parts
)
@staticmethod
def
parse_interval
(
text
,
period_keys
=
None
):
if
period_keys
is
None
:
period_keys
=
PERIOD_KEYS
regex
=
Period
.
build_regex
(
period_keys
)
def
parse_interval
(
text
):
period_keys
=
Config
.
period_keys
regex
=
Period
.
build_regex
()
match
=
re
.
fullmatch
(
regex
,
text
)
if
match
is
None
:
raise
Exception
(
"
Invalid interval config:
'
{}
'
,
"
...
...
@@ -294,36 +365,6 @@ class Period:
seconds
+=
period_keys
[
key
]
*
int
(
groups
[
key
])
return
timedelta
(
seconds
=
seconds
)
def
load_config
():
import
sys
import
toml
config_path
=
"
config.toml
"
global_config_path
=
"
/etc/lvm-snapshot.toml
"
ENV_VAR
=
"
LVM_SNAPSHOT_CONFIG
"
if
not
os
.
path
.
isfile
(
config_path
)
and
os
.
path
.
isfile
(
global_config_path
):
config_path
=
global_config_path
if
ENV_VAR
in
os
.
environ
:
config_path
=
os
.
environ
[
ENV_VAR
]
with
open
(
config_path
,
"
r
"
)
as
config_file
:
return
toml
.
load
(
config_file
)
def
parse_config
(
config
):
periods
=
{}
min_interval
=
None
for
volume_conf
in
config
[
"
volume
"
]:
name
=
volume_conf
[
"
name
"
]
volume_group
=
volume_conf
[
"
volume_group
"
]
volume
=
Volume
(
volume_group
,
name
)
periods
[
volume
]
=
[]
for
raw_period
in
volume_conf
[
"
keep
"
]:
interval
=
Period
.
parse_interval
(
raw_period
[
"
interval
"
])
if
min_interval
is
None
or
interval
<
min_interval
:
min_interval
=
interval
number
=
int
(
raw_period
[
"
number
"
])
periods
[
volume
].
append
(
Period
(
target_number
=
number
,
interval
=
interval
))
return
periods
,
min_interval
def
mark_snapshots
(
snapshots
,
periods
,
min_interval
):
all_snapshots
=
set
(
snapshots
)
marked_snapshots
=
set
()
...
...
@@ -342,29 +383,28 @@ def mark_snapshots(snapshots, periods, min_interval):
unmarked_snapshots
=
all_snapshots
-
marked_snapshots
return
unmarked_snapshots
def
list_snapshots
(
**
kwargs
):
def
list_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
print
(
"
volume: {}
"
.
format
(
str
(
volume
)))
for
snapshot
in
snapshots
[
volume
]:
print
(
"
{}
"
.
format
(
str
(
snapshot
)))
def
mount_snapshots
(
**
kwargs
):
def
mount_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
for
snapshot
in
snapshots
[
volume
]:
snapshot
.
mount
()
def
unmount_snapshots
(
**
kwargs
):
def
unmount_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
for
snapshot
in
snapshots
[
volume
]:
snapshot
.
unmount
()
def
update_snapshots
():
config
=
load_config
()
snapshots
=
Snapshot
.
list_snapshots
()
periods
,
min_interval
=
parse_config
(
config
)
periods
,
min_interval
=
Config
.
periods
,
Config
.
min_interval
for
volume
in
set
(
periods
.
keys
())
-
set
(
snapshots
.
keys
()):
logging
.
warn
(
"
Warning: Volume {} is configured but does not exist or has no snapshots.
"
.
format
(
volume
))
for
volume
in
set
(
snapshots
.
keys
())
-
set
(
periods
.
keys
()):
...
...
@@ -372,8 +412,10 @@ def update_snapshots():
snapshots
.
pop
(
volume
)
for
volume
in
snapshots
:
unmarked_snapshots
=
mark_snapshots
(
snapshots
[
volume
],
periods
[
volume
],
min_interval
)
logging
.
info
(
"
removing snapshots {}
"
.
format
(
unmarked_snapshots
))
for
snapshot
in
unmarked_snapshots
:
snapshot
.
remove
()
logging
.
info
(
"
creating new snapshot
"
)
new_snapshot
=
Snapshot
(
volume
,
datetime
.
now
())
new_snapshot
.
create
()
...
...
@@ -386,14 +428,20 @@ operations = {
def
main
():
import
argparse
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
"
command
"
,
help
=
"
list
|
update
|
mount
|
unmount
"
)
parser
.
add_argument
(
"
--verbose
"
,
action
=
"
store_true
"
,
help
=
"
do not redirect the command output to /dev/null
"
)
parser
=
argparse
.
ArgumentParser
(
description
=
"
Manage snapshots of thin LVM volumes
"
)
parser
.
add_argument
(
"
command
"
,
choices
=
[
"
list
"
,
"
update
"
,
"
mount
"
,
"
unmount
"
],
default
=
"
list
"
)
parser
.
add_argument
(
"
--verbose
"
,
"
-v
"
,
action
=
"
count
"
,
help
=
"
do not redirect the command output to /dev/null
"
)
args
=
parser
.
parse_args
()
loglevel
=
logging
.
ERROR
if
args
.
verbose
:
loglevel
=
logging
.
INFO
loglevels
=
{
0
:
logging
.
ERROR
,
1
:
logging
.
WARNING
,
2
:
logging
.
INFO
,
3
:
logging
.
DEBUG
}
loglevel
=
loglevels
[
min
(
3
,
max
(
0
,
args
.
verbose
))]
logging
.
basicConfig
(
level
=
loglevel
)
Config
.
parse
(
Config
.
load
())
operations
[
args
.
command
]()
if
__name__
==
"
__main__
"
:
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment