cli: Add service subcommand
With command `sudo nmstatectl service -c <config_folder>`, nmstatectl will search all `.yml` files in specified folder(default to `/etc/nmstate`), and apply those state files. Any applied state file will be then renamed to `.applied` suffix. The config files are sorted by rust native lexicographical order. Man page for `nmstatectl` updated. Add new man page `nmstate.service(8)`. Systemd unit file included. RPM spec file updated. Integration test case included. Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
parent
be217fdf39
commit
5fa085ae6f
21
Makefile
21
Makefile
@ -15,6 +15,7 @@ RUST_DEBUG_BIN_DIR=./target/debug
|
||||
RUST_RELEASE_BIN_DIR=./target/release
|
||||
CLI_EXEC=nmstatectl
|
||||
CLI_EXEC2=nmstate-autoconf
|
||||
SYSTEMD_SERVICE_FILE=$(ROOT_DIR)/packaging/nmstate.service
|
||||
CLIB_HEADER=rust/src/clib/nmstate.h
|
||||
CLIB_SO_DEV_RELEASE=rust/target/release/$(CLIB_SO_DEV)
|
||||
CLIB_SO_DEV_DEBUG=rust/target/debug/$(CLIB_SO_DEV)
|
||||
@ -24,9 +25,11 @@ PYTHON_MODULE_NAME=libnmstate
|
||||
PYTHON_MODULE_SRC=src/python/libnmstate
|
||||
CLI_EXEC_RELEASE=rust/target/release/$(CLI_EXEC)
|
||||
PREFIX ?= /usr/local
|
||||
SYSTEMD_UNIT_DIR ?= $(PREFIX)/lib/systemd/system
|
||||
GO_MODULE_SRC ?= rust/src/go/nmstate
|
||||
CLI_MANPAGE=doc/nmstatectl.8
|
||||
CLI_MANPAGE2=doc/nmstate-autoconf.8
|
||||
SYSTEMD_UNIT_MANPAGE=doc/nmstate.service.8
|
||||
SPEC_FILE=packaging/nmstate.spec
|
||||
RPM_DATA=$(shell date +"%a %b %d %Y")
|
||||
|
||||
@ -71,7 +74,14 @@ $(CLI_MANPAGE2): $(CLI_MANPAGE2).in
|
||||
sed -i -e "s/@DATE@/$(shell date +'%B %d, %Y')/" $(CLI_MANPAGE2)
|
||||
sed -i -e "s/@VERSION@/$(VERSION)/" $(CLI_MANPAGE2)
|
||||
|
||||
manpage: $(CLI_MANPAGE) $(CLI_MANPAGE2)
|
||||
.PHONY: $(SYSTEMD_UNIT_MANPAGE)
|
||||
$(SYSTEMD_UNIT_MANPAGE): $(SYSTEMD_UNIT_MANPAGE).in
|
||||
cp $(SYSTEMD_UNIT_MANPAGE).in $(SYSTEMD_UNIT_MANPAGE)
|
||||
sed -i -e "s/@DATE@/$(shell date +'%B %d, %Y')/" $(SYSTEMD_UNIT_MANPAGE)
|
||||
sed -i -e "s/@VERSION@/$(VERSION)/" $(SYSTEMD_UNIT_MANPAGE)
|
||||
|
||||
|
||||
manpage: $(CLI_MANPAGE) $(CLI_MANPAGE2) $(SYSTEMD_UNIT_MANPAGE)
|
||||
clib: $(CLIB_HEADER) $(CLIB_SO_DEV_RELEASE) $(CLIB_PKG_CONFIG)
|
||||
|
||||
.PHONY: $(SPEC_FILE)
|
||||
@ -113,8 +123,10 @@ dist: manpage $(SPEC_FILE) $(CLIB_HEADER)
|
||||
tar x -C $(TMPDIR)
|
||||
cp $(CLI_MANPAGE) $(TMPDIR)/nmstate-$(VERSION)/doc/
|
||||
cp $(CLI_MANPAGE2) $(TMPDIR)/nmstate-$(VERSION)/doc/
|
||||
cp $(SYSTEMD_UNIT_MANPAGE) $(TMPDIR)/nmstate-$(VERSION)/doc/
|
||||
cp $(SPEC_FILE) $(TMPDIR)/nmstate-$(VERSION)/packaging/
|
||||
cp $(CLIB_HEADER) $(TMPDIR)/nmstate-$(VERSION)/rust/src/clib/
|
||||
cp $(SYSTEMD_SERVICE_FILE) $(TMPDIR)/nmstate-$(VERSION)/
|
||||
cd $(TMPDIR) && tar cfz $(TARBALL) nmstate-$(VERSION)/
|
||||
mv $(TMPDIR)/$(TARBALL) ./
|
||||
if [ $(SKIP_VENDOR_CREATION) == 0 ];then \
|
||||
@ -196,6 +208,7 @@ check: rust_check clib_check go_check
|
||||
clean:
|
||||
rm -f $(CLI_MANPAGE)
|
||||
rm -f $(CLI_MANPAGE2)
|
||||
rm -f $(SYSTEMD_UNIT_MANPAGE)
|
||||
rm -f $(SPEC_FILE)
|
||||
rm -f $(TARBALL)
|
||||
cd rust && cargo clean || true
|
||||
@ -228,6 +241,11 @@ install: $(CLI_EXEC_RELEASE) manpage clib
|
||||
install -p -v -D -m644 $(CLI_MANPAGE2) \
|
||||
$(DESTDIR)$(MAN_DIR)/man8/$(shell basename $(CLI_MANPAGE2))
|
||||
gzip $(DESTDIR)$(MAN_DIR)/man8/$(shell basename $(CLI_MANPAGE2))
|
||||
install -p -v -D -m644 $(SYSTEMD_UNIT_MANPAGE) \
|
||||
$(DESTDIR)$(MAN_DIR)/man8/$(shell basename $(SYSTEMD_UNIT_MANPAGE))
|
||||
gzip $(DESTDIR)$(MAN_DIR)/man8/$(shell basename $(SYSTEMD_UNIT_MANPAGE))
|
||||
install -p -v -D -m644 $(SYSTEMD_SERVICE_FILE) \
|
||||
$(DESTDIR)$(SYSTEMD_UNIT_DIR)/$(shell basename $(SYSTEMD_SERVICE_FILE))
|
||||
|
||||
|
||||
uninstall:
|
||||
@ -244,3 +262,4 @@ uninstall:
|
||||
- if [ $(SKIP_PYTHON_INSTALL) != 1 ];then \
|
||||
rm -rfv $(DESTDIR)$(PYTHON3_SITE_DIR)/$(PYTHON_MODULE_NAME); \
|
||||
fi
|
||||
- rm -fv $(DESTDIR)$(SYSTEMD_UNIT_DIR)/$(shell basename $(SYSTEMD_SERVICE_FILE))
|
||||
|
20
doc/nmstate.service.8.in
Normal file
20
doc/nmstate.service.8.in
Normal file
@ -0,0 +1,20 @@
|
||||
.TH nmstate\&.service 8 "@DATE@" "@VERSION@" "nmstate.service man page"
|
||||
.SH "NAME"
|
||||
nmstate\&.service \- Apply /etc/nmstate network states
|
||||
.SH "SYNOPSIS"
|
||||
.PP
|
||||
nmstate\&.service
|
||||
.SH "DESCRIPTION"
|
||||
.PP
|
||||
nmstate\&.service invokes \fBnmstatectl service\fR command which
|
||||
apply all network state files ending with \fB.yml\fR in
|
||||
\fB/etc/nmstate\fR folder.
|
||||
The applied network state file will be renamed with postfix \fB.applied\fR
|
||||
to prevent repeated applied on next service start.
|
||||
.SH BUG REPORTS
|
||||
Report bugs on nmstate GitHub issues <https://github.com/nmstate/nmstate>.
|
||||
.SH COPYRIGHT
|
||||
License Apache 2.0 or any later version
|
||||
<https://www.apache.org/licenses/LICENSE-2.0.txt>
|
||||
.SH SEE ALSO
|
||||
.B nmstatectl\fP(8)
|
@ -19,6 +19,8 @@ nmstatectl \- A nmstate command line tool
|
||||
.br
|
||||
.B nmstatectl commit \fR[\fICHECKPOINT_PATH\fR]
|
||||
.br
|
||||
.B nmstatectl service \fR[\fI-c, --config <CONFIG_FOLDER>\fR]
|
||||
.br
|
||||
.B nmstatectl version
|
||||
.br
|
||||
.SH DESCRIPTION
|
||||
@ -111,6 +113,15 @@ checkpoint if not defined as argument.
|
||||
.RS
|
||||
displays nmstate version.
|
||||
.RE
|
||||
|
||||
.B service
|
||||
.RS
|
||||
Apply all network state files ending with \fB.yml\fR in specified(
|
||||
default: \fB/etc/nmstate\fR) folder.
|
||||
The applied network state file will be renamed with postfix \fB.applied\fR
|
||||
to prevent repeated applied on next run.
|
||||
.RE
|
||||
|
||||
.PP
|
||||
.RE
|
||||
.SH OPTIONS
|
||||
|
12
packaging/nmstate.service
Normal file
12
packaging/nmstate.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Apply nmstate on-disk state
|
||||
Documentation=man:nmstate.service(8) https://www.nmstate.io
|
||||
After=NetworkManager.service
|
||||
Requires=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/nmstatectl service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -14,6 +14,7 @@ URL: https://github.com/%{srcname}/%{srcname}
|
||||
Source0: https://github.com/%{srcname}/%{srcname}/releases/download/v%{version}/%{srcname}-%{version}.tar.gz
|
||||
BuildRequires: python3-setuptools
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: systemd
|
||||
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
|
||||
%if 0%{?rhel}
|
||||
BuildRequires: rust-toolset
|
||||
@ -91,10 +92,12 @@ env SKIP_PYTHON_INSTALL=1 PREFIX=%{_prefix} LIBDIR=%{_libdir} %make_install
|
||||
%files
|
||||
%doc README.md
|
||||
%doc examples/
|
||||
%{_mandir}/man8/nmstate.service.8*
|
||||
%{_mandir}/man8/nmstatectl.8*
|
||||
%{_mandir}/man8/nmstate-autoconf.8*
|
||||
%{_bindir}/nmstatectl
|
||||
%{_bindir}/nmstate-autoconf
|
||||
%{_unitdir}/nmstate.service
|
||||
|
||||
%files libs
|
||||
%{_libdir}/libnmstate.so.*
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod autoconf;
|
||||
mod error;
|
||||
mod service;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{self, stdin, stdout, Read, Write};
|
||||
@ -26,6 +27,7 @@ const SUB_CMD_ROLLBACK: &str = "rollback";
|
||||
const SUB_CMD_EDIT: &str = "edit";
|
||||
const SUB_CMD_VERSION: &str = "version";
|
||||
const SUB_CMD_AUTOCONF: &str = "autoconf";
|
||||
const SUB_CMD_SERVICE: &str = "service";
|
||||
|
||||
const EX_DATAERR: i32 = 65;
|
||||
const EXIT_FAILURE: i32 = 1;
|
||||
@ -229,6 +231,19 @@ fn main() {
|
||||
.help("Do not make the state persistent"),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
clap::Command::new(SUB_CMD_SERVICE)
|
||||
.about("Service mode: apply files from service folder")
|
||||
.arg(
|
||||
clap::Arg::new(self::service::CONFIG_FOLDER_KEY)
|
||||
.long("config")
|
||||
.short('c')
|
||||
.required(false)
|
||||
.takes_value(true)
|
||||
.default_value(self::service::DEFAULT_SERVICE_FOLDER)
|
||||
.help("Folder hold network state files"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
clap::Command::new(SUB_CMD_VERSION)
|
||||
.about("Show version")
|
||||
@ -286,6 +301,11 @@ fn main() {
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches(SUB_CMD_EDIT) {
|
||||
print_result_and_exit(state_edit(matches), EX_DATAERR);
|
||||
} else if let Some(matches) = matches.subcommand_matches(SUB_CMD_SERVICE) {
|
||||
print_result_and_exit(
|
||||
self::service::ncl_service(matches),
|
||||
EXIT_FAILURE,
|
||||
);
|
||||
} else if matches.subcommand_matches(SUB_CMD_VERSION).is_some() {
|
||||
print_string_and_exit(format!(
|
||||
"{} {}",
|
||||
|
86
rust/src/cli/service.rs
Normal file
86
rust/src/cli/service.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nmstate::NetworkState;
|
||||
|
||||
use crate::error::CliError;
|
||||
|
||||
pub(crate) const DEFAULT_SERVICE_FOLDER: &str = "/etc/nmstate";
|
||||
pub(crate) const CONFIG_FOLDER_KEY: &str = "CONFIG_FOLDER";
|
||||
|
||||
const CONFIG_FILE_EXTENTION: &str = "yml";
|
||||
const RELOCATE_FILE_EXTENTION: &str = "applied";
|
||||
|
||||
pub(crate) fn ncl_service(
|
||||
matches: &clap::ArgMatches,
|
||||
) -> Result<String, CliError> {
|
||||
let folder = matches
|
||||
.value_of(CONFIG_FOLDER_KEY)
|
||||
.unwrap_or(DEFAULT_SERVICE_FOLDER);
|
||||
|
||||
let config_files = get_config_files(folder)?;
|
||||
if config_files.is_empty() {
|
||||
log::info!(
|
||||
"No nmstate config(end with .{}) found in config folder {}",
|
||||
CONFIG_FILE_EXTENTION,
|
||||
folder
|
||||
);
|
||||
}
|
||||
|
||||
for file_path in config_files {
|
||||
match apply_file(&file_path) {
|
||||
Ok(()) => {
|
||||
log::info!("Applied nmstate config: {}", file_path.display());
|
||||
if let Err(e) = relocate_file(&file_path) {
|
||||
log::error!(
|
||||
"Failed to rename applied state file: {} {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to apply state file {}: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
// All file ending with `.yml` will be included.
|
||||
fn get_config_files(folder: &str) -> Result<Vec<PathBuf>, CliError> {
|
||||
let folder = Path::new(folder);
|
||||
let mut ret = Vec::new();
|
||||
for entry in folder.read_dir()? {
|
||||
let file = entry?.path();
|
||||
if file.extension() == Some(OsStr::new(CONFIG_FILE_EXTENTION)) {
|
||||
ret.push(folder.join(file));
|
||||
}
|
||||
}
|
||||
ret.sort_unstable();
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
// rename file by adding a suffix `.applied`.
|
||||
fn relocate_file(file_path: &Path) -> Result<(), CliError> {
|
||||
let new_path = file_path.with_extension(RELOCATE_FILE_EXTENTION);
|
||||
std::fs::rename(file_path, &new_path)?;
|
||||
log::info!(
|
||||
"Renamed applied config {} to {}",
|
||||
file_path.display(),
|
||||
new_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_file(file_path: &Path) -> Result<(), CliError> {
|
||||
let fd = std::fs::File::open(file_path)?;
|
||||
let net_state: NetworkState = serde_yaml::from_reader(fd)?;
|
||||
net_state.apply()?;
|
||||
Ok(())
|
||||
}
|
117
tests/integration/nmstate_service_test.py
Normal file
117
tests/integration/nmstate_service_test.py
Normal file
@ -0,0 +1,117 @@
|
||||
#
|
||||
# Copyright (c) 2022 Red Hat, Inc.
|
||||
#
|
||||
# This file is part of nmstate
|
||||
#
|
||||
# This program 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 program 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 program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
import yaml
|
||||
import pytest
|
||||
|
||||
import libnmstate
|
||||
from libnmstate.schema import Interface
|
||||
from libnmstate.schema import InterfaceState
|
||||
|
||||
from .testlib.cmdlib import exec_cmd
|
||||
from .testlib.assertlib import assert_state_match
|
||||
|
||||
TEST_YAML1_CONTENT = """
|
||||
---
|
||||
interfaces:
|
||||
- name: dummy0
|
||||
type: dummy
|
||||
state: up
|
||||
ipv4:
|
||||
enabled: false
|
||||
ipv6:
|
||||
enabled: false
|
||||
"""
|
||||
|
||||
TEST_YAML2_CONTENT = """
|
||||
---
|
||||
interfaces:
|
||||
- name: dummy0
|
||||
type: dummy
|
||||
state: up
|
||||
ipv4:
|
||||
address:
|
||||
- ip: 192.0.2.252
|
||||
prefix-length: 24
|
||||
- ip: 192.0.2.251
|
||||
prefix-length: 24
|
||||
dhcp: false
|
||||
enabled: true
|
||||
ipv6:
|
||||
address:
|
||||
- ip: 2001:db8:2::1
|
||||
prefix-length: 64
|
||||
- ip: 2001:db8:1::1
|
||||
prefix-length: 64
|
||||
autoconf: false
|
||||
dhcp: false
|
||||
enabled: true
|
||||
"""
|
||||
|
||||
CONFIG_DIR = "/etc/nmstate"
|
||||
TEST_CONFIG1_FILE_PATH = f"{CONFIG_DIR}/01-nmstate-test.yml"
|
||||
TEST_CONFIG1_APPLIED_FILE_PATH = f"{CONFIG_DIR}/01-nmstate-test.applied"
|
||||
TEST_CONFIG2_FILE_PATH = f"{CONFIG_DIR}/02-nmstate-test.yml"
|
||||
TEST_CONFIG2_APPLIED_FILE_PATH = f"{CONFIG_DIR}/02-nmstate-test.applied"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nmstate_etc_config():
|
||||
if not os.path.isdir(CONFIG_DIR):
|
||||
os.mkdir(CONFIG_DIR)
|
||||
|
||||
for file_path, content in [
|
||||
(
|
||||
TEST_CONFIG1_FILE_PATH,
|
||||
TEST_YAML1_CONTENT,
|
||||
),
|
||||
(
|
||||
TEST_CONFIG2_FILE_PATH,
|
||||
TEST_YAML2_CONTENT,
|
||||
),
|
||||
]:
|
||||
with open(file_path, "w") as fd:
|
||||
fd.write(content)
|
||||
yield
|
||||
libnmstate.apply(
|
||||
{
|
||||
Interface.KEY: [
|
||||
{
|
||||
Interface.NAME: "dummy0",
|
||||
Interface.STATE: InterfaceState.ABSENT,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
os.remove(TEST_CONFIG1_APPLIED_FILE_PATH)
|
||||
os.remove(TEST_CONFIG2_APPLIED_FILE_PATH)
|
||||
|
||||
|
||||
def test_nmstate_service_apply(nmstate_etc_config):
|
||||
exec_cmd("systemctl start nmstate".split(), check=True)
|
||||
|
||||
desire_state = yaml.load(TEST_YAML2_CONTENT, Loader=yaml.SafeLoader)
|
||||
assert_state_match(desire_state)
|
||||
|
||||
assert not os.path.exists(TEST_CONFIG1_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG1_APPLIED_FILE_PATH)
|
||||
assert not os.path.exists(TEST_CONFIG2_FILE_PATH)
|
||||
assert os.path.isfile(TEST_CONFIG2_APPLIED_FILE_PATH)
|
Loading…
x
Reference in New Issue
Block a user