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/
...
@@ -2,3 +2,4 @@ bin/
lib/
lib/
share/
share/
local/
local/
config.toml
This diff is collapsed.
Click to expand it.
config.example.toml
+
1
−
0
View file @
beec5311
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
[[volume]]
[[volume]]
volume_group
=
"exports"
volume_group
=
"exports"
name
=
"pub"
name
=
"pub"
path
=
".snapshots"
[[volume.keep]]
[[volume.keep]]
number
=
24
number
=
24
...
...
This diff is collapsed.
Click to expand it.
lvmsnapshot.py
+
118
−
70
View file @
beec5311
...
@@ -31,17 +31,56 @@ import json
...
@@ -31,17 +31,56 @@ import json
from
contextlib
import
contextmanager
from
contextlib
import
contextmanager
from
collections
import
OrderedDict
from
collections
import
OrderedDict
SNAPSHOT_BASE_DIR
=
"
/snapshots
"
class
Config
:
TIMESTAMP_FORMAT
=
"
%Y-%m-%d-%H-%M
"
snapshot_base_dir
=
"
/snapshots
"
PERIOD_KEYS
=
OrderedDict
([
timestamp_format
=
"
%Y-%m-%d-%H-%M
"
period_keys
=
OrderedDict
([
(
"
y
"
,
365
*
86400
),
(
"
y
"
,
365
*
86400
),
(
"
m
"
,
31
*
86400
),
(
"
m
"
,
31
*
86400
),
(
"
d
"
,
86400
),
(
"
d
"
,
86400
),
(
"
H
"
,
3600
),
(
"
H
"
,
3600
),
(
"
M
"
,
60
),
(
"
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
):
def
run_process
(
command
,
check
=
True
):
logging
.
debug
(
"
running command {}
"
.
format
(
command
))
result
=
sp
.
run
(
command
,
check
=
check
,
stdout
=
sp
.
PIPE
,
stderr
=
sp
.
PIPE
)
result
=
sp
.
run
(
command
,
check
=
check
,
stdout
=
sp
.
PIPE
,
stderr
=
sp
.
PIPE
)
if
result
.
stdout
:
if
result
.
stdout
:
logging
.
info
(
result
.
stdout
.
decode
(
"
utf-8
"
).
strip
())
logging
.
info
(
result
.
stdout
.
decode
(
"
utf-8
"
).
strip
())
...
@@ -64,9 +103,17 @@ def freeze_xfs(mountpoint, freeze):
...
@@ -64,9 +103,17 @@ def freeze_xfs(mountpoint, freeze):
run_process
(
command
,
check
=
True
)
run_process
(
command
,
check
=
True
)
class
Volume
:
class
Volume
:
def
__init__
(
self
,
volume_group
,
name
):
def
__init__
(
self
,
volume_group
,
name
,
snapshot_dir
=
None
):
self
.
volume_group
=
volume_group
self
.
volume_group
=
volume_group
self
.
name
=
name
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
):
def
__repr__
(
self
):
return
self
.
get_full_name
()
return
self
.
get_full_name
()
...
@@ -85,6 +132,7 @@ class Volume:
...
@@ -85,6 +132,7 @@ class Volume:
def
get_mountpoint
(
self
):
def
get_mountpoint
(
self
):
mapper_device
=
self
.
get_mapper_device
()
mapper_device
=
self
.
get_mapper_device
()
logging
.
debug
(
"
searching volume mountpoint for {}
"
.
format
(
mapper_device
))
command
=
[
command
=
[
"
/bin/findmnt
"
,
"
/bin/findmnt
"
,
"
--source
"
,
mapper_device
,
"
--source
"
,
mapper_device
,
...
@@ -110,7 +158,7 @@ class Snapshot:
...
@@ -110,7 +158,7 @@ class Snapshot:
return
self
.
get_name
()
return
self
.
get_name
()
def
get_timestamp_str
(
self
):
def
get_timestamp_str
(
self
):
return
self
.
timestamp
.
strftime
(
TIMESTAMP_FORMAT
)
return
self
.
timestamp
.
strftime
(
Config
.
timestamp_format
)
def
get_name
(
self
):
def
get_name
(
self
):
return
"
{}-snapshot-{}
"
.
format
(
return
"
{}-snapshot-{}
"
.
format
(
...
@@ -120,7 +168,7 @@ class Snapshot:
...
@@ -120,7 +168,7 @@ class Snapshot:
@staticmethod
@staticmethod
def
parse_name
(
name
):
def
parse_name
(
name
):
parent_name
,
_
,
timestamp_str
=
name
.
split
(
"
-
"
,
2
)
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
return
parent_name
,
timestamp
def
get_full_volume
(
self
):
def
get_full_volume
(
self
):
...
@@ -128,12 +176,30 @@ class Snapshot:
...
@@ -128,12 +176,30 @@ class Snapshot:
def
get_mountpoint
(
self
):
def
get_mountpoint
(
self
):
return
os
.
path
.
join
(
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
):
def
create_mountpoint
(
self
):
logging
.
debug
(
"
creating mountpoint {}
"
.
format
(
self
.
get_mountpoint
()))
os
.
makedirs
(
self
.
get_mountpoint
(),
mode
=
0o755
,
exist_ok
=
True
)
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
):
def
get_mapper_device
(
self
):
return
"
/dev/mapper/{}-{}
"
.
format
(
return
"
/dev/mapper/{}-{}
"
.
format
(
self
.
parent_volume
.
volume_group
,
self
.
parent_volume
.
volume_group
,
...
@@ -149,6 +215,7 @@ class Snapshot:
...
@@ -149,6 +215,7 @@ class Snapshot:
self
.
get_mapper_device
())
self
.
get_mapper_device
())
def
create
(
self
):
def
create
(
self
):
logging
.
debug
(
"
creating snapshot {}
"
.
format
(
self
.
get_name
()))
parent_mountpoint
=
self
.
parent_volume
.
get_mountpoint
()
parent_mountpoint
=
self
.
parent_volume
.
get_mountpoint
()
#with xfs_freeze(parent_mountpoint): # not necessary, lvm does this
#with xfs_freeze(parent_mountpoint): # not necessary, lvm does this
create_command
=
[
create_command
=
[
...
@@ -162,7 +229,9 @@ class Snapshot:
...
@@ -162,7 +229,9 @@ class Snapshot:
self
.
mount
()
self
.
mount
()
def
mount
(
self
):
def
mount
(
self
):
logging
.
debug
(
"
mounting snapshot {}
"
.
format
(
self
.
get_name
()))
if
self
.
check_mount
():
if
self
.
check_mount
():
logging
.
debug
(
"
already mounted
"
)
return
return
activate_command
=
[
activate_command
=
[
"
/sbin/lvchange
"
,
"
/sbin/lvchange
"
,
...
@@ -193,6 +262,7 @@ class Snapshot:
...
@@ -193,6 +262,7 @@ class Snapshot:
return
False
return
False
def
remove
(
self
):
def
remove
(
self
):
logging
.
debug
(
"
removing snapshot {}
"
.
format
(
self
.
get_name
()))
self
.
unmount
()
self
.
unmount
()
remove_command
=
[
remove_command
=
[
"
/sbin/lvremove
"
,
"
/sbin/lvremove
"
,
...
@@ -201,12 +271,17 @@ class Snapshot:
...
@@ -201,12 +271,17 @@ class Snapshot:
run_process
(
remove_command
,
check
=
True
)
run_process
(
remove_command
,
check
=
True
)
def
unmount
(
self
):
def
unmount
(
self
):
logging
.
debug
(
"
unmounting snapshot {}
"
.
format
(
self
.
get_name
()))
mountpoint
=
self
.
find_mountpoint
()
if
mountpoint
is
not
None
:
if
self
.
check_mount
():
if
self
.
check_mount
():
unmount_command
=
[
unmount_command
=
[
"
/bin/umount
"
,
"
/bin/umount
"
,
self
.
get_
mountpoint
()
mountpoint
]
]
run_process
(
unmount_command
,
check
=
True
)
run_process
(
unmount_command
,
check
=
True
)
if
os
.
path
.
isdir
(
mountpoint
):
os
.
rmdir
(
mountpoint
)
deactivate_command
=
[
deactivate_command
=
[
"
/sbin/lvchange
"
,
"
/sbin/lvchange
"
,
"
--activate
"
,
"
n
"
,
"
--activate
"
,
"
n
"
,
...
@@ -215,7 +290,6 @@ class Snapshot:
...
@@ -215,7 +290,6 @@ class Snapshot:
]
]
run_process
(
deactivate_command
,
check
=
True
)
run_process
(
deactivate_command
,
check
=
True
)
self
.
active
=
False
self
.
active
=
False
os
.
rmdir
(
self
.
get_mountpoint
())
@staticmethod
@staticmethod
def
list_snapshots
():
def
list_snapshots
():
...
@@ -272,17 +346,14 @@ class Period:
...
@@ -272,17 +346,14 @@ class Period:
return
datetime
.
now
()
-
self
.
target_number
*
self
.
interval
return
datetime
.
now
()
-
self
.
target_number
*
self
.
interval
@staticmethod
@staticmethod
def
build_regex
(
period_keys
=
None
):
def
build_regex
():
if
period_keys
is
None
:
parts
=
[
r
"
(?:(?P<{0}>\d+){0})?
"
.
format
(
key
)
for
key
in
Config
.
period_keys
]
period_keys
=
PERIOD_KEYS
parts
=
[
r
"
(?:(?P<{0}>\d+){0})?
"
.
format
(
key
)
for
key
in
period_keys
]
return
""
.
join
(
parts
)
return
""
.
join
(
parts
)
@staticmethod
@staticmethod
def
parse_interval
(
text
,
period_keys
=
None
):
def
parse_interval
(
text
):
if
period_keys
is
None
:
period_keys
=
Config
.
period_keys
period_keys
=
PERIOD_KEYS
regex
=
Period
.
build_regex
()
regex
=
Period
.
build_regex
(
period_keys
)
match
=
re
.
fullmatch
(
regex
,
text
)
match
=
re
.
fullmatch
(
regex
,
text
)
if
match
is
None
:
if
match
is
None
:
raise
Exception
(
"
Invalid interval config:
'
{}
'
,
"
raise
Exception
(
"
Invalid interval config:
'
{}
'
,
"
...
@@ -294,36 +365,6 @@ class Period:
...
@@ -294,36 +365,6 @@ class Period:
seconds
+=
period_keys
[
key
]
*
int
(
groups
[
key
])
seconds
+=
period_keys
[
key
]
*
int
(
groups
[
key
])
return
timedelta
(
seconds
=
seconds
)
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
):
def
mark_snapshots
(
snapshots
,
periods
,
min_interval
):
all_snapshots
=
set
(
snapshots
)
all_snapshots
=
set
(
snapshots
)
marked_snapshots
=
set
()
marked_snapshots
=
set
()
...
@@ -342,29 +383,28 @@ def mark_snapshots(snapshots, periods, min_interval):
...
@@ -342,29 +383,28 @@ def mark_snapshots(snapshots, periods, min_interval):
unmarked_snapshots
=
all_snapshots
-
marked_snapshots
unmarked_snapshots
=
all_snapshots
-
marked_snapshots
return
unmarked_snapshots
return
unmarked_snapshots
def
list_snapshots
(
**
kwargs
):
def
list_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
for
volume
in
snapshots
:
print
(
"
volume: {}
"
.
format
(
str
(
volume
)))
print
(
"
volume: {}
"
.
format
(
str
(
volume
)))
for
snapshot
in
snapshots
[
volume
]:
for
snapshot
in
snapshots
[
volume
]:
print
(
"
{}
"
.
format
(
str
(
snapshot
)))
print
(
"
{}
"
.
format
(
str
(
snapshot
)))
def
mount_snapshots
(
**
kwargs
):
def
mount_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
for
volume
in
snapshots
:
for
snapshot
in
snapshots
[
volume
]:
for
snapshot
in
snapshots
[
volume
]:
snapshot
.
mount
()
snapshot
.
mount
()
def
unmount_snapshots
(
**
kwargs
):
def
unmount_snapshots
():
snapshots
=
Snapshot
.
list_snapshots
()
snapshots
=
Snapshot
.
list_snapshots
()
for
volume
in
snapshots
:
for
volume
in
snapshots
:
for
snapshot
in
snapshots
[
volume
]:
for
snapshot
in
snapshots
[
volume
]:
snapshot
.
unmount
()
snapshot
.
unmount
()
def
update_snapshots
():
def
update_snapshots
():
config
=
load_config
()
snapshots
=
Snapshot
.
list_snapshots
()
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
()):
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
))
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
()):
for
volume
in
set
(
snapshots
.
keys
())
-
set
(
periods
.
keys
()):
...
@@ -372,8 +412,10 @@ def update_snapshots():
...
@@ -372,8 +412,10 @@ def update_snapshots():
snapshots
.
pop
(
volume
)
snapshots
.
pop
(
volume
)
for
volume
in
snapshots
:
for
volume
in
snapshots
:
unmarked_snapshots
=
mark_snapshots
(
snapshots
[
volume
],
periods
[
volume
],
min_interval
)
unmarked_snapshots
=
mark_snapshots
(
snapshots
[
volume
],
periods
[
volume
],
min_interval
)
logging
.
info
(
"
removing snapshots {}
"
.
format
(
unmarked_snapshots
))
for
snapshot
in
unmarked_snapshots
:
for
snapshot
in
unmarked_snapshots
:
snapshot
.
remove
()
snapshot
.
remove
()
logging
.
info
(
"
creating new snapshot
"
)
new_snapshot
=
Snapshot
(
volume
,
datetime
.
now
())
new_snapshot
=
Snapshot
(
volume
,
datetime
.
now
())
new_snapshot
.
create
()
new_snapshot
.
create
()
...
@@ -386,14 +428,20 @@ operations = {
...
@@ -386,14 +428,20 @@ operations = {
def
main
():
def
main
():
import
argparse
import
argparse
parser
=
argparse
.
ArgumentParser
()
parser
=
argparse
.
ArgumentParser
(
description
=
"
Manage snapshots of thin LVM volumes
"
)
parser
.
add_argument
(
"
command
"
,
help
=
"
list
|
update
|
mount
|
unmount
"
)
parser
.
add_argument
(
"
command
"
,
choices
=
[
"
list
"
,
"
update
"
,
"
mount
"
,
"
unmount
"
],
default
=
"
list
"
)
parser
.
add_argument
(
"
--verbose
"
,
action
=
"
store_true
"
,
help
=
"
do not redirect the command output to /dev/null
"
)
parser
.
add_argument
(
"
--verbose
"
,
"
-v
"
,
action
=
"
count
"
,
help
=
"
do not redirect the command output to /dev/null
"
)
args
=
parser
.
parse_args
()
args
=
parser
.
parse_args
()
loglevel
=
logging
.
ERROR
loglevels
=
{
if
args
.
verbose
:
0
:
logging
.
ERROR
,
loglevel
=
logging
.
INFO
1
:
logging
.
WARNING
,
2
:
logging
.
INFO
,
3
:
logging
.
DEBUG
}
loglevel
=
loglevels
[
min
(
3
,
max
(
0
,
args
.
verbose
))]
logging
.
basicConfig
(
level
=
loglevel
)
logging
.
basicConfig
(
level
=
loglevel
)
Config
.
parse
(
Config
.
load
())
operations
[
args
.
command
]()
operations
[
args
.
command
]()
if
__name__
==
"
__main__
"
:
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