Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F117520282
config_mgmt.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
28 KB
Referenced Files
None
Subscribers
None
config_mgmt.py
View Options
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>.
import
os
import
re
import
sys
import
gzip
import
logging
from
typing
import
Optional
from
typing
import
Tuple
from
filecmp
import
cmp
from
datetime
import
datetime
from
textwrap
import
dedent
from
pathlib
import
Path
from
shutil
import
copy
,
chown
from
subprocess
import
Popen
from
subprocess
import
DEVNULL
from
urllib.parse
import
urlsplit
from
urllib.parse
import
urlunsplit
from
tabulate
import
tabulate
from
vyos.config
import
Config
from
vyos.configtree
import
ConfigTree
from
vyos.configtree
import
ConfigTreeError
from
vyos.configsession
import
ConfigSession
from
vyos.configsession
import
ConfigSessionError
from
vyos.configtree
import
diff_compare
from
vyos.load_config
import
load
from
vyos.load_config
import
LoadConfigError
from
vyos.defaults
import
directories
from
vyos.version
import
get_full_version_data
from
vyos.utils.io
import
ask_yes_no
from
vyos.utils.boot
import
boot_configuration_complete
from
vyos.utils.process
import
is_systemd_service_active
from
vyos.utils.process
import
rc_cmd
from
vyos.defaults
import
DEFAULT_COMMIT_CONFIRM_MINUTES
from
vyos.component_version
import
append_system_version
from
vyos.utils.file
import
file_compare
SAVE_CONFIG
=
'/usr/libexec/vyos/vyos-save-config.py'
config_json
=
'/run/vyatta/config/config.json'
# created by vyatta-cfg-postinst
commit_post_hook_dir
=
'/etc/commit/post-hooks.d'
commit_hooks
=
{
'commit_revision'
:
'01vyos-commit-revision'
,
'commit_archive'
:
'02vyos-commit-archive'
,
}
timer_name
=
'commit-confirm'
config_file
=
os
.
path
.
join
(
directories
[
'config'
],
'config.boot'
)
archive_dir
=
os
.
path
.
join
(
directories
[
'config'
],
'archive'
)
archive_config_file
=
os
.
path
.
join
(
archive_dir
,
'config.boot'
)
commit_log_file
=
os
.
path
.
join
(
archive_dir
,
'commits'
)
logrotate_conf
=
os
.
path
.
join
(
archive_dir
,
'lr.conf'
)
logrotate_state
=
os
.
path
.
join
(
archive_dir
,
'lr.state'
)
rollback_config
=
os
.
path
.
join
(
archive_dir
,
'config.boot-rollback'
)
prerollback_config
=
os
.
path
.
join
(
archive_dir
,
'config.boot-prerollback'
)
tmp_log_entry
=
'/tmp/commit-rev-entry'
logger
=
logging
.
getLogger
(
'config_mgmt'
)
logger
.
setLevel
(
logging
.
INFO
)
ch
=
logging
.
StreamHandler
()
formatter
=
logging
.
Formatter
(
'
%(funcName)s
:
%(levelname)s
:
%(message)s
'
)
ch
.
setFormatter
(
formatter
)
logger
.
addHandler
(
ch
)
def
save_config
(
target
,
json_out
=
None
):
if
json_out
is
None
:
cmd
=
f
'{SAVE_CONFIG} {target}'
else
:
cmd
=
f
'{SAVE_CONFIG} {target} --write-json-file {json_out}'
rc
,
out
=
rc_cmd
(
cmd
)
if
rc
!=
0
:
logger
.
critical
(
f
'save config failed: {out}'
)
def
unsaved_commits
(
allow_missing_config
=
False
)
->
bool
:
if
get_full_version_data
()[
'boot_via'
]
==
'livecd'
:
return
False
if
allow_missing_config
and
not
os
.
path
.
exists
(
config_file
):
return
True
tmp_save
=
'/tmp/config.running'
save_config
(
tmp_save
)
ret
=
not
file_compare
(
tmp_save
,
config_file
)
os
.
unlink
(
tmp_save
)
return
ret
def
get_file_revision
(
rev
:
int
):
revision
=
os
.
path
.
join
(
archive_dir
,
f
'config.boot.{rev}.gz'
)
try
:
with
gzip
.
open
(
revision
)
as
f
:
r
=
f
.
read
()
.
decode
()
except
FileNotFoundError
:
logger
.
warning
(
f
'commit revision {rev} not available'
)
return
''
return
r
def
get_config_tree_revision
(
rev
:
int
):
c
=
get_file_revision
(
rev
)
return
ConfigTree
(
c
)
def
is_node_revised
(
path
:
list
=
[],
rev1
:
int
=
1
,
rev2
:
int
=
0
)
->
bool
:
from
vyos.configtree
import
DiffTree
left
=
get_config_tree_revision
(
rev1
)
right
=
get_config_tree_revision
(
rev2
)
diff_tree
=
DiffTree
(
left
,
right
)
if
diff_tree
.
add
.
exists
(
path
)
or
diff_tree
.
sub
.
exists
(
path
):
return
True
return
False
class
ConfigMgmtError
(
Exception
):
pass
class
ConfigMgmt
:
def
__init__
(
self
,
session_env
=
None
,
config
=
None
):
if
session_env
:
self
.
_session_env
=
session_env
else
:
self
.
_session_env
=
None
if
config
is
None
:
config
=
Config
()
d
=
config
.
get_config_dict
(
[
'system'
,
'config-management'
],
key_mangling
=
(
'-'
,
'_'
),
get_first_key
=
True
,
with_recursive_defaults
=
True
,
)
self
.
max_revisions
=
int
(
d
.
get
(
'commit_revisions'
,
0
))
self
.
num_revisions
=
0
self
.
locations
=
d
.
get
(
'commit_archive'
,
{})
.
get
(
'location'
,
[])
self
.
source_address
=
d
.
get
(
'commit_archive'
,
{})
.
get
(
'source_address'
,
''
)
self
.
reboot_unconfirmed
=
bool
(
d
.
get
(
'commit_confirm'
,
{})
.
get
(
'action'
)
==
'reboot'
)
self
.
config_dict
=
d
if
config
.
exists
([
'system'
,
'host-name'
]):
self
.
hostname
=
config
.
return_value
([
'system'
,
'host-name'
])
if
config
.
exists
([
'system'
,
'domain-name'
]):
tmp
=
config
.
return_value
([
'system'
,
'domain-name'
])
self
.
hostname
+=
f
'.{tmp}'
else
:
self
.
hostname
=
'vyos'
# upload only on existence of effective values, notably, on boot.
# one still needs session self.locations (above) for setting
# post-commit hook in conf_mode script
path
=
[
'system'
,
'config-management'
,
'commit-archive'
,
'location'
]
if
config
.
exists_effective
(
path
):
self
.
effective_locations
=
config
.
return_effective_values
(
path
)
else
:
self
.
effective_locations
=
[]
# a call to compare without args is edit_level aware
edit_level
=
os
.
getenv
(
'VYATTA_EDIT_LEVEL'
,
''
)
self
.
edit_path
=
[
l
for
l
in
edit_level
.
split
(
'/'
)
if
l
]
# noqa: E741
self
.
active_config
=
config
.
_running_config
self
.
working_config
=
config
.
_session_config
# Console script functions
#
def
commit_confirm
(
self
,
minutes
:
int
=
DEFAULT_COMMIT_CONFIRM_MINUTES
,
no_prompt
:
bool
=
False
)
->
Tuple
[
str
,
int
]:
"""Commit with reload/reboot to saved config in 'minutes' minutes if
'confirm' call is not issued.
"""
if
is_systemd_service_active
(
f
'{timer_name}.timer'
):
msg
=
'Another confirm is pending'
return
msg
,
1
if
self
.
reboot_unconfirmed
and
unsaved_commits
():
W
=
'
\n
You should save previous commits before commit-confirm !
\n
'
else
:
W
=
''
if
self
.
reboot_unconfirmed
:
prompt_str
=
f
"""
commit-confirm will automatically reboot in {minutes} minutes unless changes
are confirmed.
Proceed ?"""
else
:
prompt_str
=
f
"""
commit-confirm will automatically reload previous config in {minutes} minutes
unless changes are confirmed.
Proceed ?"""
prompt_str
=
W
+
prompt_str
if
not
no_prompt
and
not
ask_yes_no
(
prompt_str
,
default
=
True
):
msg
=
'commit-confirm canceled'
return
msg
,
1
if
self
.
reboot_unconfirmed
:
action
=
'sg vyattacfg "/usr/bin/config-mgmt revert"'
else
:
action
=
'sg vyattacfg "/usr/bin/config-mgmt revert_soft"'
cmd
=
f
'sudo systemd-run --quiet --on-active={minutes}m --unit={timer_name} {action}'
rc
,
out
=
rc_cmd
(
cmd
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
# start notify
if
self
.
reboot_unconfirmed
:
cmd
=
(
f
'sudo -b /usr/libexec/vyos/commit-confirm-notify.py --reboot {minutes}'
)
else
:
cmd
=
f
'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}'
Popen
(
cmd
.
split
(),
stdout
=
DEVNULL
,
stderr
=
DEVNULL
,
stdin
=
DEVNULL
,
close_fds
=
True
,
preexec_fn
=
os
.
setsid
,
)
if
self
.
reboot_unconfirmed
:
msg
=
f
'Initialized commit-confirm; {minutes} minutes to confirm before reboot'
else
:
msg
=
f
'Initialized commit-confirm; {minutes} minutes to confirm before reload'
return
msg
,
0
def
confirm
(
self
)
->
Tuple
[
str
,
int
]:
"""Do not reboot/reload to saved/completed config following 'commit-confirm'.
Update commit log and archive.
"""
if
not
is_systemd_service_active
(
f
'{timer_name}.timer'
):
msg
=
'No confirm pending'
return
msg
,
0
cmd
=
f
'sudo systemctl stop --quiet {timer_name}.timer'
rc
,
out
=
rc_cmd
(
cmd
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
# kill notify
cmd
=
'sudo pkill -f commit-confirm-notify.py'
rc
,
out
=
rc_cmd
(
cmd
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
entry
=
self
.
_read_tmp_log_entry
()
if
self
.
_archive_active_config
():
self
.
_add_log_entry
(
**
entry
)
self
.
_update_archive
()
if
self
.
reboot_unconfirmed
:
msg
=
'Reboot timer stopped'
else
:
msg
=
'Reload timer stopped'
return
msg
,
0
def
revert
(
self
)
->
Tuple
[
str
,
int
]:
"""Reboot to saved config, dropping commits from 'commit-confirm'."""
_
=
self
.
_read_tmp_log_entry
()
# archived config will be reverted on boot
rc
,
out
=
rc_cmd
(
'sudo systemctl reboot'
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
return
''
,
0
def
revert_soft
(
self
)
->
Tuple
[
str
,
int
]:
"""Reload last revision, dropping commits from 'commit-confirm'."""
_
=
self
.
_read_tmp_log_entry
()
# commits under commit-confirm are not added to revision list unless
# confirmed, hence a soft revert is to revision 0
revert_ct
=
self
.
get_config_tree_revision
(
0
)
message
=
'[commit-confirm] Reverting to previous config now'
os
.
system
(
'wall -n '
+
message
)
mask
=
os
.
umask
(
0
o002
)
session
=
ConfigSession
(
os
.
getpid
(),
app
=
'config-mgmt'
)
try
:
if
session
.
vyconf_backend
():
session
.
load_config_obj
(
revert_ct
)
else
:
session
.
load_explicit
(
revert_ct
)
session
.
commit
()
except
ConfigSessionError
as
e
:
raise
ConfigMgmtError
(
e
)
from
e
finally
:
os
.
umask
(
mask
)
del
session
return
''
,
0
def
rollback
(
self
,
rev
:
int
,
no_prompt
:
bool
=
False
)
->
Tuple
[
str
,
int
]:
"""Reboot to config revision 'rev'."""
msg
=
''
if
not
self
.
_check_revision_number
(
rev
):
msg
=
(
f
'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
)
return
msg
,
1
prompt_str
=
'Proceed with reboot ?'
if
not
no_prompt
and
not
ask_yes_no
(
prompt_str
,
default
=
True
):
msg
=
'Canceling rollback'
return
msg
,
0
rc
,
out
=
rc_cmd
(
f
'sudo cp {archive_config_file} {prerollback_config}'
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
path
=
os
.
path
.
join
(
archive_dir
,
f
'config.boot.{rev}.gz'
)
with
gzip
.
open
(
path
)
as
f
:
config
=
f
.
read
()
try
:
with
open
(
rollback_config
,
'wb'
)
as
f
:
f
.
write
(
config
)
copy
(
rollback_config
,
config_file
)
except
OSError
as
e
:
raise
ConfigMgmtError
from
e
rc
,
out
=
rc_cmd
(
'sudo systemctl reboot'
)
if
rc
!=
0
:
raise
ConfigMgmtError
(
out
)
return
msg
,
0
def
rollback_soft
(
self
,
rev
:
int
):
"""Rollback without reboot (rollback-soft)"""
msg
=
''
if
not
self
.
_check_revision_number
(
rev
):
msg
=
(
f
'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
)
return
msg
,
1
rollback_ct
=
self
.
get_config_tree_revision
(
rev
)
try
:
load
(
rollback_ct
,
switch
=
'explicit'
)
print
(
'Rollback diff has been applied.'
)
print
(
'Use "compare" to review the changes or "commit" to apply them.'
)
except
LoadConfigError
as
e
:
raise
ConfigMgmtError
(
e
)
from
e
return
msg
,
0
def
compare
(
self
,
saved
:
bool
=
False
,
commands
:
bool
=
False
,
rev1
:
Optional
[
int
]
=
None
,
rev2
:
Optional
[
int
]
=
None
,
)
->
Tuple
[
str
,
int
]:
"""General compare function for config file revisions:
revision n vs. revision m; working version vs. active version;
or working version vs. saved version.
"""
ct1
=
self
.
active_config
ct2
=
self
.
working_config
msg
=
'No changes between working and active configurations.
\n
'
if
saved
:
ct1
=
self
.
_get_saved_config_tree
()
ct2
=
self
.
working_config
msg
=
'No changes between working and saved configurations.
\n
'
if
rev1
is
not
None
:
if
not
self
.
_check_revision_number
(
rev1
):
return
f
'Invalid revision number {rev1}'
,
1
ct1
=
self
.
get_config_tree_revision
(
rev1
)
ct2
=
self
.
working_config
msg
=
f
'No changes between working and revision {rev1} configurations.
\n
'
if
rev2
is
not
None
:
if
not
self
.
_check_revision_number
(
rev2
):
return
f
'Invalid revision number {rev2}'
,
1
# compare older to newer
ct2
=
ct1
ct1
=
self
.
get_config_tree_revision
(
rev2
)
msg
=
f
'No changes between revisions {rev2} and {rev1} configurations.
\n
'
out
=
''
path
=
[]
if
commands
else
self
.
edit_path
try
:
if
commands
:
out
=
diff_compare
(
ct1
,
ct2
,
path
=
path
,
commands
=
True
)
else
:
out
=
diff_compare
(
ct1
,
ct2
,
path
=
path
)
except
ConfigTreeError
as
e
:
return
e
,
1
if
out
:
msg
=
out
return
msg
,
0
def
wrap_compare
(
self
,
options
)
->
Tuple
[
str
,
int
]:
"""Interface to vyatta-cfg-run: args collected as 'options' to parse
for compare.
"""
cmnds
=
False
r1
=
None
r2
=
None
if
'commands'
in
options
:
cmnds
=
True
options
.
remove
(
'commands'
)
for
i
in
options
:
if
not
i
.
isnumeric
():
options
.
remove
(
i
)
if
len
(
options
)
>
0
:
r1
=
int
(
options
[
0
])
if
len
(
options
)
>
1
:
r2
=
int
(
options
[
1
])
return
self
.
compare
(
commands
=
cmnds
,
rev1
=
r1
,
rev2
=
r2
)
# Initialization and post-commit hooks for conf-mode
#
def
initialize_revision
(
self
):
"""Initialize config archive, logrotate conf, and commit log."""
mask
=
os
.
umask
(
0
o002
)
os
.
makedirs
(
archive_dir
,
exist_ok
=
True
)
json_dir
=
os
.
path
.
dirname
(
config_json
)
try
:
os
.
makedirs
(
json_dir
,
exist_ok
=
True
)
chown
(
json_dir
,
group
=
'vyattacfg'
)
except
OSError
as
e
:
logger
.
warning
(
f
'cannot create {json_dir}: {e}'
)
self
.
_add_logrotate_conf
()
if
not
os
.
path
.
exists
(
commit_log_file
)
or
self
.
_get_number_of_revisions
()
==
0
:
user
=
self
.
_get_user
()
via
=
'init'
comment
=
''
# add empty init config before boot-config load for revision
# and diff consistency
if
self
.
_archive_active_config
():
self
.
_add_log_entry
(
user
,
via
,
comment
)
self
.
_update_archive
()
os
.
umask
(
mask
)
def
commit_revision
(
self
):
"""Update commit log and rotate archived config.boot.
commit_revision is called in post-commit-hooks, if
['commit-archive', 'commit-revisions'] is configured.
"""
if
os
.
getenv
(
'IN_COMMIT_CONFIRM'
,
''
):
self
.
_new_log_entry
(
tmp_file
=
tmp_log_entry
)
return
if
self
.
_archive_active_config
():
self
.
_add_log_entry
()
self
.
_update_archive
()
def
commit_archive
(
self
):
"""Upload config to remote archive."""
from
vyos.remote
import
upload
hostname
=
self
.
hostname
t
=
datetime
.
now
()
timestamp
=
t
.
strftime
(
'%Y%m
%d
_%H%M%S'
)
remote_file
=
f
'config.boot-{hostname}.{timestamp}'
source_address
=
self
.
source_address
if
self
.
effective_locations
:
print
(
'Archiving config...'
)
for
location
in
self
.
effective_locations
:
url
=
urlsplit
(
location
)
_
,
_
,
netloc
=
url
.
netloc
.
rpartition
(
'@'
)
redacted_location
=
urlunsplit
(
url
.
_replace
(
netloc
=
netloc
))
print
(
f
' {redacted_location}'
,
end
=
' '
,
flush
=
True
)
upload
(
archive_config_file
,
f
'{location}/{remote_file}'
,
source_host
=
source_address
,
)
# op-mode functions
#
def
get_raw_log_data
(
self
)
->
list
:
"""Return list of dicts of log data:
keys: [timestamp, user, commit_via, commit_comment]
"""
log
=
self
.
_get_log_entries
()
res_l
=
[]
for
line
in
log
:
d
=
self
.
_get_log_entry
(
line
)
res_l
.
append
(
d
)
return
res_l
@staticmethod
def
format_log_data
(
data
:
list
)
->
str
:
"""Return formatted log data as str."""
res_l
=
[]
for
l_no
,
l_val
in
enumerate
(
data
):
time_d
=
datetime
.
fromtimestamp
(
int
(
l_val
[
'timestamp'
]))
time_str
=
time_d
.
strftime
(
'%Y-%m-
%d
%H:%M:%S'
)
res_l
.
append
(
[
l_no
,
time_str
,
f
"by {l_val['user']}"
,
f
"via {l_val['commit_via']}"
]
)
if
l_val
[
'commit_comment'
]
!=
'commit'
:
# default comment
res_l
.
append
([
None
,
l_val
[
'commit_comment'
]])
ret
=
tabulate
(
res_l
,
tablefmt
=
'plain'
)
return
ret
@staticmethod
def
format_log_data_brief
(
data
:
list
)
->
str
:
"""Return 'brief' form of log data as str.
Slightly compacted format used in completion help for
'rollback'.
"""
res_l
=
[]
for
l_no
,
l_val
in
enumerate
(
data
):
time_d
=
datetime
.
fromtimestamp
(
int
(
l_val
[
'timestamp'
]))
time_str
=
time_d
.
strftime
(
'%Y-%m-
%d
%H:%M:%S'
)
res_l
.
append
(
[
'
\t
'
,
l_no
,
time_str
,
f
"{l_val['user']}"
,
f
"by {l_val['commit_via']}"
]
)
ret
=
tabulate
(
res_l
,
tablefmt
=
'plain'
)
return
ret
def
show_commit_diff
(
self
,
rev
:
int
,
rev2
:
Optional
[
int
]
=
None
,
commands
:
bool
=
False
)
->
str
:
"""Show commit diff at revision number, compared to previous
revision, or to another revision.
"""
if
rev2
is
None
:
out
,
_
=
self
.
compare
(
commands
=
commands
,
rev1
=
rev
,
rev2
=
(
rev
+
1
))
return
out
out
,
_
=
self
.
compare
(
commands
=
commands
,
rev1
=
rev
,
rev2
=
rev2
)
return
out
def
show_commit_file
(
self
,
rev
:
int
)
->
str
:
return
self
.
_get_file_revision
(
rev
)
# utility functions
#
def
_get_saved_config_tree
(
self
):
with
open
(
config_file
)
as
f
:
c
=
f
.
read
()
return
ConfigTree
(
c
)
def
_get_file_revision
(
self
,
rev
:
int
):
if
rev
not
in
range
(
0
,
self
.
_get_number_of_revisions
()):
raise
ConfigMgmtError
(
'revision not available'
)
revision
=
os
.
path
.
join
(
archive_dir
,
f
'config.boot.{rev}.gz'
)
with
gzip
.
open
(
revision
)
as
f
:
r
=
f
.
read
()
.
decode
()
return
r
def
get_config_tree_revision
(
self
,
rev
:
int
):
c
=
self
.
_get_file_revision
(
rev
)
return
ConfigTree
(
c
)
def
_add_logrotate_conf
(
self
):
conf
:
str
=
dedent
(
f
"""\
{archive_config_file} {{
su root vyattacfg
rotate {self.max_revisions}
start 0
compress
copy
}}
"""
)
conf_file
=
Path
(
logrotate_conf
)
conf_file
.
write_text
(
conf
)
conf_file
.
chmod
(
0
o644
)
def
_archive_active_config
(
self
)
->
bool
:
# on first boot/fresh install, add baseline archive_config_file
if
not
os
.
path
.
exists
(
archive_config_file
):
append_system_version
(
archive_config_file
)
mask
=
os
.
umask
(
0
o113
)
ext
=
os
.
getpid
()
cmp_saved
=
f
'/tmp/config.boot.{ext}'
if
boot_configuration_complete
():
save_config
(
cmp_saved
,
json_out
=
config_json
)
else
:
copy
(
config_file
,
cmp_saved
)
# on boot, we need to manually create the config.json file; after
# boot, it is written by save_config, above
if
not
os
.
path
.
exists
(
config_json
):
ct
=
self
.
_get_saved_config_tree
()
try
:
with
open
(
config_json
,
'w'
)
as
f
:
f
.
write
(
ct
.
to_json
())
chown
(
config_json
,
group
=
'vyattacfg'
)
except
OSError
as
e
:
logger
.
warning
(
f
'cannot create {config_json}: {e}'
)
try
:
if
cmp
(
cmp_saved
,
archive_config_file
,
shallow
=
False
):
os
.
unlink
(
cmp_saved
)
os
.
umask
(
mask
)
return
False
except
FileNotFoundError
:
pass
rc
,
out
=
rc_cmd
(
f
'sudo mv {cmp_saved} {archive_config_file}'
)
os
.
umask
(
mask
)
if
rc
!=
0
:
logger
.
critical
(
f
'mv file to archive failed: {out}'
)
return
False
return
True
@staticmethod
def
_update_archive
():
cmd
=
f
'sudo logrotate -f -s {logrotate_state} {logrotate_conf}'
rc
,
out
=
rc_cmd
(
cmd
)
if
rc
!=
0
:
logger
.
critical
(
f
'logrotate failure: {out}'
)
@staticmethod
def
_get_log_entries
()
->
list
:
"""Return lines of commit log as list of strings"""
entries
=
[]
if
os
.
path
.
exists
(
commit_log_file
):
with
open
(
commit_log_file
)
as
f
:
entries
=
f
.
readlines
()
return
entries
def
_get_number_of_revisions
(
self
)
->
int
:
log_entries
=
self
.
_get_log_entries
()
return
len
(
log_entries
)
def
_check_revision_number
(
self
,
rev
:
int
)
->
bool
:
self
.
num_revisions
=
self
.
_get_number_of_revisions
()
if
not
0
<=
rev
<
self
.
num_revisions
:
return
False
return
True
@staticmethod
def
_get_user
()
->
str
:
import
pwd
try
:
user
=
os
.
getlogin
()
except
OSError
:
try
:
user
=
pwd
.
getpwuid
(
os
.
geteuid
())[
0
]
except
KeyError
:
user
=
'unknown'
return
user
def
_new_log_entry
(
self
,
user
:
str
=
''
,
commit_via
:
str
=
''
,
commit_comment
:
str
=
''
,
timestamp
:
Optional
[
int
]
=
None
,
tmp_file
:
str
=
None
,
)
->
Optional
[
str
]:
# Format log entry and return str or write to file.
#
# Usage is within a post-commit hook, using env values. In case of
# commit-confirm, it can be written to a temporary file for
# inclusion on 'confirm'.
from
time
import
time
if
timestamp
is
None
:
timestamp
=
int
(
time
())
if
not
user
:
user
=
self
.
_get_user
()
if
not
commit_via
:
commit_via
=
os
.
getenv
(
'COMMIT_VIA'
,
'other'
)
if
not
commit_comment
:
commit_comment
=
os
.
getenv
(
'COMMIT_COMMENT'
,
'commit'
)
# the commit log reserves '|' as field demarcation, so replace in
# comment if present; undo this in _get_log_entry, below
if
re
.
search
(
r'\|'
,
commit_comment
):
commit_comment
=
commit_comment
.
replace
(
'|'
,
'
%%
'
)
entry
=
f
'|{timestamp}|{user}|{commit_via}|{commit_comment}|
\n
'
mask
=
os
.
umask
(
0
o113
)
if
tmp_file
is
not
None
:
try
:
with
open
(
tmp_file
,
'w'
)
as
f
:
f
.
write
(
entry
)
except
OSError
as
e
:
logger
.
critical
(
f
'write to {tmp_file} failed: {e}'
)
os
.
umask
(
mask
)
return
None
os
.
umask
(
mask
)
return
entry
@staticmethod
def
_get_log_entry
(
line
:
str
)
->
dict
:
log_fmt
=
re
.
compile
(
r'\|.*\|\n?$'
)
keys
=
[
'user'
,
'commit_via'
,
'commit_comment'
,
'timestamp'
]
if
not
log_fmt
.
match
(
line
):
logger
.
critical
(
f
'Invalid log format {line}'
)
return
{}
timestamp
,
user
,
commit_via
,
commit_comment
=
tuple
(
line
.
strip
()
.
strip
(
'|'
)
.
split
(
'|'
)
)
commit_comment
=
commit_comment
.
replace
(
'
%%
'
,
'|'
)
d
=
dict
(
zip
(
keys
,
[
user
,
commit_via
,
commit_comment
,
timestamp
]))
return
d
def
_read_tmp_log_entry
(
self
)
->
dict
:
try
:
with
open
(
tmp_log_entry
)
as
f
:
entry
=
f
.
read
()
os
.
unlink
(
tmp_log_entry
)
except
OSError
as
e
:
logger
.
info
(
f
'error on file {tmp_log_entry}: {e}'
)
# fail gracefully in corner case:
# delete commit-revisions; commit-confirm
return
{}
return
self
.
_get_log_entry
(
entry
)
def
_add_log_entry
(
self
,
user
:
str
=
''
,
commit_via
:
str
=
''
,
commit_comment
:
str
=
''
,
timestamp
:
Optional
[
int
]
=
None
,
):
mask
=
os
.
umask
(
0
o113
)
entry
=
self
.
_new_log_entry
(
user
=
user
,
commit_via
=
commit_via
,
commit_comment
=
commit_comment
,
timestamp
=
timestamp
,
)
log_entries
=
self
.
_get_log_entries
()
log_entries
.
insert
(
0
,
entry
)
if
len
(
log_entries
)
>
self
.
max_revisions
:
log_entries
=
log_entries
[:
-
1
]
try
:
with
open
(
commit_log_file
,
'w'
)
as
f
:
f
.
writelines
(
log_entries
)
except
OSError
as
e
:
logger
.
critical
(
e
)
os
.
umask
(
mask
)
# entry_point for console script
#
def
run
():
from
argparse
import
ArgumentParser
,
REMAINDER
config_mgmt
=
ConfigMgmt
()
for
s
in
list
(
commit_hooks
):
if
sys
.
argv
[
0
]
.
replace
(
'-'
,
'_'
)
.
endswith
(
s
):
func
=
getattr
(
config_mgmt
,
s
)
try
:
func
()
except
Exception
as
e
:
print
(
f
'{s}: {e}'
)
sys
.
exit
(
0
)
parser
=
ArgumentParser
()
subparsers
=
parser
.
add_subparsers
(
dest
=
'subcommand'
)
commit_confirm
=
subparsers
.
add_parser
(
'commit_confirm'
,
help
=
'Commit with opt-out reboot to saved config'
)
commit_confirm
.
add_argument
(
'-t'
,
dest
=
'minutes'
,
type
=
int
,
default
=
DEFAULT_COMMIT_CONFIRM_MINUTES
,
help
=
"Minutes until reboot, unless 'confirm'"
,
)
commit_confirm
.
add_argument
(
'-y'
,
dest
=
'no_prompt'
,
action
=
'store_true'
,
help
=
'Execute without prompt'
)
subparsers
.
add_parser
(
'confirm'
,
help
=
'Confirm commit'
)
subparsers
.
add_parser
(
'revert'
,
help
=
'Revert commit-confirm with reboot'
)
subparsers
.
add_parser
(
'revert_soft'
,
help
=
'Revert commit-confirm with reload'
)
rollback
=
subparsers
.
add_parser
(
'rollback'
,
help
=
'Rollback to earlier config'
)
rollback
.
add_argument
(
'--rev'
,
type
=
int
,
help
=
'Revision number for rollback'
)
rollback
.
add_argument
(
'-y'
,
dest
=
'no_prompt'
,
action
=
'store_true'
,
help
=
'Excute without prompt'
)
rollback_soft
=
subparsers
.
add_parser
(
'rollback_soft'
,
help
=
'Rollback to earlier config'
)
rollback_soft
.
add_argument
(
'--rev'
,
type
=
int
,
help
=
'Revision number for rollback'
)
compare
=
subparsers
.
add_parser
(
'compare'
,
help
=
'Compare config files'
)
compare
.
add_argument
(
'--saved'
,
action
=
'store_true'
,
help
=
'Compare session config with saved config'
)
compare
.
add_argument
(
'--commands'
,
action
=
'store_true'
,
help
=
'Show difference between commands'
)
compare
.
add_argument
(
'--rev1'
,
type
=
int
,
default
=
None
,
help
=
'Compare revision with session config or other revision'
,
)
compare
.
add_argument
(
'--rev2'
,
type
=
int
,
default
=
None
,
help
=
'Compare revisions'
)
wrap_compare
=
subparsers
.
add_parser
(
'wrap_compare'
,
help
=
'Wrapper interface for vyatta-cfg-run'
)
wrap_compare
.
add_argument
(
'--options'
,
nargs
=
REMAINDER
)
args
=
vars
(
parser
.
parse_args
())
func
=
getattr
(
config_mgmt
,
args
[
'subcommand'
])
del
args
[
'subcommand'
]
res
=
''
try
:
res
,
rc
=
func
(
**
args
)
except
ConfigMgmtError
as
e
:
print
(
e
)
sys
.
exit
(
1
)
if
res
:
print
(
res
)
sys
.
exit
(
rc
)
File Metadata
Details
Attached
Mime Type
text/x-script.python
Expires
Sat, Sep 26, 9:48 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4284833
Default Alt Text
config_mgmt.py (28 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment