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:
Gris Ge 2022-06-24 16:38:45 +08:00
parent be217fdf39
commit 5fa085ae6f
8 changed files with 289 additions and 1 deletions

View File

@ -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
View 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)

View File

@ -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
View 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

View File

@ -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.*

View File

@ -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
View 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(())
}

View 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)