-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Configure NetworkManager using static network interface definitions (#1)
* Initial implementation * Add content verification tests * Fix destination path * Add CI workflows (#2) * Add build & test CI workflow * Add release CI workflow * Fix build command * Add content write permissions to the release workflow * Add documentation (#3) * Update README * Fix examples and styling * Use lower case formatting for MAC addresses (#4) * Fix connection files permissions (#5) * Improve error handling (#6) * Add missing error formatting * Collect copying errors instead of terminating the execution * Improve logging * Add configuration logs * Improve log formatting * Update README
- Loading branch information
1 parent
0fb074e
commit f86471f
Showing
19 changed files
with
854 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
on: [push, pull_request] | ||
name: Build & Test | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Go | ||
uses: actions/setup-go@v4 | ||
with: | ||
go-version: '1.20' | ||
- name: Build | ||
run: go build -v ./... | ||
- name: Test | ||
run: go test -v ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
on: | ||
push: | ||
tags: | ||
- 'v*' | ||
|
||
name: Release | ||
|
||
permissions: | ||
contents: write | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Go | ||
uses: actions/setup-go@v4 | ||
with: | ||
go-version: '1.20' | ||
- name: Run tests | ||
run: go test -v ./... | ||
- name: Build binaries | ||
run: | | ||
GOOS=linux GOARCH=arm64 go build -o nm-configurator-arm64 main.go | ||
GOOS=linux GOARCH=amd64 go build -o nm-configurator-amd64 main.go | ||
- name: Create a release | ||
uses: softprops/action-gh-release@v1 | ||
with: | ||
files: | | ||
nm-configurator-arm64 | ||
nm-configurator-amd64 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,111 @@ | ||
# nm-configurator | ||
NetworkManager configuration tool | ||
|
||
A tool capable of identifying & storing the relevant NetworkManager settings | ||
for a given host out of a pool of predefined desired configurations. | ||
|
||
Typically used with [Combustion](https://documentation.suse.com/sle-micro/5.4/single-html/SLE-Micro-deployment/#cha-images-combustion) | ||
in order to bootstrap multiple nodes using the same provisioning artefact instead of depending on different custom images per machine. | ||
|
||
## What are the prerequisites? | ||
|
||
### Desired network configurations per host | ||
|
||
`nm-configurator` depends on having the desired network state for all known nodes beforehand. | ||
|
||
[NetworkManager](https://documentation.suse.com/sle-micro/5.4/html/SLE-Micro-all/cha-nm-configuration.html) | ||
is using connection profiles defined as files stored under `/etc/NetworkManager/system-connections`. | ||
These config files (*.nmconnection) can be generated using [nmstate](https://nmstate.io/features/gen_conf.html). | ||
|
||
Each file contains the desired state for a single network interface (e.g. `eth0`). | ||
Configurations for all interfaces for all known hosts must be generated using `nmstate`. | ||
|
||
### Network interface mapping | ||
|
||
Network interface mapping is required in order for `nm-configurator` | ||
to identify the proper configurations for each host it is running on. | ||
|
||
This additional config must be provided in a YAML format mapping the logical name of the interface to its MAC address: | ||
|
||
```yaml | ||
host_config: | ||
- hostname: node1.example.com | ||
interfaces: | ||
- logical_name: eth0 | ||
mac_address: 00:10:20:30:40:50 | ||
- logical_name: eth1 | ||
mac_address: 10:20:30:40:50:60 | ||
- hostname: node2.example.com | ||
interfaces: | ||
- logical_name: eth0 | ||
mac_address: 00:11:22:33:44:55 | ||
``` | ||
**NOTE:** Interface names during the installation of nodes might differ from the preconfigured logical ones. | ||
This is expected and `nm-configurator` will rely on the MAC addresses and use the actual names for the | ||
NetworkManager configurations instead e.g. settings for interface with a predefined logical name `eth0` but | ||
actually named `eth0.101` will automatically be adjusted and stored to `/etc/NetworkManager/eth0.101.nmconnection`. | ||
|
||
## How to install it? | ||
|
||
### Standard method: | ||
|
||
Each release is published with `nm-configurator` already built for `amd64` and `arm64` Linux systems: | ||
|
||
For AMD64 / x86_64 based systems: | ||
```shell | ||
$ curl -o nm-configurator -L https://github.com/suse-edge/nm-configurator/releases/latest/download/nm-configurator-amd64 | ||
$ chmod +x nm-configurator | ||
``` | ||
|
||
For ARM64 based systems: | ||
```shell | ||
$ curl -o nm-configurator -L https://github.com/suse-edge/nm-configurator/releases/latest/download/nm-configurator-arm64 | ||
$ chmod +x nm-configurator | ||
``` | ||
|
||
### Manual method: | ||
|
||
```shell | ||
$ git clone https://github.com/suse-edge/nm-configurator.git | ||
$ cd nm-configurator | ||
$ go build . # optionally specify GOOS and GOARCH flags if cross compiling | ||
``` | ||
|
||
## How to run it? | ||
|
||
Using an example configuration of three known nodes (with hostnames `node1.example.com`, `node2.example.com` | ||
and `node3.example.com` and their respective NetworkManager settings) and interface mapping defined in `host_config.yaml`: | ||
|
||
```text | ||
config | ||
├── node1.example.com | ||
│ ├── eth0.nmconnection | ||
│ └── eth1.nmconnection | ||
├── node2.example.com | ||
│ └── eth0.nmconnection | ||
├── node3.example.com | ||
│ ├── bond0.nmconnection | ||
│ └── eth1.nmconnection | ||
└── host_config.yaml | ||
``` | ||
|
||
```shell | ||
$ ./nm-configurator -config-dir=config -hosts-config-file=host_config.yaml | ||
INFO[2023-08-17T17:32:23+03:00] starting network manager configurator... | ||
INFO[2023-08-17T17:32:23+03:00] successfully identified host: node1.example.com | ||
INFO[2023-08-17T17:32:23+03:00] storing file /etc/NetworkManager/system-connections/eth0.nmconnection... | ||
INFO[2023-08-17T17:32:23+03:00] storing file /etc/NetworkManager/system-connections/eth1.nmconnection... | ||
INFO[2023-08-17T17:32:23+03:00] successfully configured network manager | ||
``` | ||
|
||
*Note:* The default values for `-config-dir` and `-hosts-config-file` flags are `config` and `host_config.yaml` | ||
respectively so providing them is not necessary with the file structure in the example: | ||
|
||
```shell | ||
$ ./nm-configurator | ||
INFO[2023-08-17T17:45:41+03:00] starting network manager configurator... | ||
INFO[2023-08-17T17:45:41+03:00] successfully identified host: node1.example.com | ||
INFO[2023-08-17T17:45:41+03:00] storing file /etc/NetworkManager/system-connections/eth0.nmconnection... | ||
INFO[2023-08-17T17:45:41+03:00] storing file /etc/NetworkManager/system-connections/eth1.nmconnection... | ||
INFO[2023-08-17T17:45:41+03:00] successfully configured network manager | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module github.com/suse-edge/nm-configurator | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/sirupsen/logrus v1.9.3 | ||
github.com/stretchr/testify v1.7.0 | ||
gopkg.in/ini.v1 v1.67.0 | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= | ||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"os" | ||
|
||
log "github.com/sirupsen/logrus" | ||
"github.com/suse-edge/nm-configurator/pkg/config" | ||
"github.com/suse-edge/nm-configurator/pkg/configurator" | ||
) | ||
|
||
const systemConnectionsDir = "/etc/NetworkManager/system-connections" | ||
|
||
func init() { | ||
log.SetFormatter(&log.TextFormatter{ | ||
FullTimestamp: true, | ||
QuoteEmptyFields: true, | ||
}) | ||
log.SetOutput(os.Stdout) | ||
} | ||
|
||
func main() { | ||
var ( | ||
configDir string | ||
hostsConfigFile string | ||
verbose bool | ||
) | ||
|
||
flag.StringVar(&configDir, "config-dir", "config", "directory storing host mapping ('host_config.yaml') and *.nmconnection files per host") | ||
flag.StringVar(&hostsConfigFile, "hosts-config-file", "host_config.yaml", "name of the hosts config file mapping interfaces to the respective MAC addresses") | ||
flag.BoolVar(&verbose, "verbose", false, "enables DEBUG log level") | ||
flag.Parse() | ||
|
||
if verbose { | ||
log.SetLevel(log.DebugLevel) | ||
} | ||
|
||
log.Info("starting network manager configurator...") | ||
|
||
if err := os.MkdirAll(systemConnectionsDir, 0755); err != nil { | ||
log.Fatalf("failed to create \"system-connections\" dir: %s", err) | ||
} | ||
|
||
conf, err := config.Load(configDir, hostsConfigFile, systemConnectionsDir) | ||
if err != nil { | ||
log.Fatalf("failed to load static host configuration: %s", err) | ||
} | ||
|
||
log.Debugf("loaded static configuration: %+v", conf) | ||
|
||
networkInterfaces, err := configurator.GetNetworkInterfaces() | ||
if err != nil { | ||
log.Fatalf("failed to list system network interfaces: %s", err) | ||
} | ||
|
||
log.Debugf("fetched system network interfaces: %+v", networkInterfaces) | ||
|
||
c := configurator.New(conf, networkInterfaces) | ||
if err = c.Run(); err != nil { | ||
log.Fatalf("failed to configure network manager: %s", err) | ||
} | ||
log.Info("successfully configured network manager") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"github.com/suse-edge/nm-configurator/pkg/config" | ||
"github.com/suse-edge/nm-configurator/pkg/configurator" | ||
) | ||
|
||
const ( | ||
sourceDir = "testdata" | ||
configFile = "host_config.yaml" | ||
destDir = "testdata/out" | ||
) | ||
|
||
func setupDestDir(t *testing.T) func(t *testing.T) { | ||
require.NoError(t, os.MkdirAll(destDir, 0755)) | ||
|
||
return func(t *testing.T) { | ||
assert.NoError(t, os.RemoveAll(destDir)) | ||
} | ||
} | ||
|
||
func TestConfigurator(t *testing.T) { | ||
teardown := setupDestDir(t) | ||
defer teardown(t) | ||
|
||
conf, err := config.Load(sourceDir, configFile, destDir) | ||
require.Nil(t, err) | ||
|
||
networkInterfaces := map[string]string{ | ||
"00:11:22:33:44:55": "eth0", | ||
"00:11:22:33:44:56": "eth0.202", // Defined as "eth0.101" in eth0.101.nmconnection | ||
"00:11:22:33:44:57": "eth1", | ||
//"00:11:22:33:44:58": "bond0", Excluded on purpose, "bond0.nmconnection" should still be copied | ||
} | ||
|
||
c := configurator.New(conf, networkInterfaces) | ||
require.NoError(t, c.Run()) | ||
|
||
// Verify the content of the copied files. | ||
hostDir := filepath.Join(sourceDir, "node1.example.com") | ||
entries, err := os.ReadDir(hostDir) | ||
require.Nil(t, err) | ||
|
||
assert.Len(t, entries, 4) | ||
|
||
for _, entry := range entries { | ||
filename := entry.Name() | ||
input, err := os.ReadFile(filepath.Join(hostDir, filename)) | ||
require.Nil(t, err) | ||
|
||
// Adjust the name and content for the "eth0.101"->"eth0.202" edge case. | ||
if filename == "eth0.101.nmconnection" { | ||
filename = "eth0.202.nmconnection" | ||
input = []byte(strings.ReplaceAll(string(input), "eth0.101", "eth0.202")) | ||
} | ||
|
||
output, err := os.ReadFile(filepath.Join(destDir, filename)) | ||
require.Nil(t, err) | ||
|
||
assert.Equal(t, string(input), string(output)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
type Config struct { | ||
// Configuration directory storing the preconfigured *.nmconnection files per host. | ||
SourceDir string | ||
// Destination directory to store the final *.nmconnection files for NetworkManager. | ||
// Default "/etc/NetworkManager/system-connections". | ||
DestinationDir string | ||
Hosts []*Host `yaml:"host_config"` | ||
} | ||
|
||
type Host struct { | ||
Name string `yaml:"hostname"` | ||
Interfaces []*Interface `yaml:"interfaces"` | ||
} | ||
|
||
func (h *Host) String() string { | ||
return fmt.Sprintf("{Name: %s Interfaces: %+v}", h.Name, h.Interfaces) | ||
} | ||
|
||
type Interface struct { | ||
LogicalName string `yaml:"logical_name"` | ||
MACAddress string `yaml:"mac_address"` | ||
} | ||
|
||
func (i *Interface) String() string { | ||
return fmt.Sprintf("{LogicalName: %s MACAddress: %s}", i.LogicalName, i.MACAddress) | ||
} | ||
|
||
func Load(sourceDir, configFilename, destinationDir string) (*Config, error) { | ||
configFile := filepath.Join(sourceDir, configFilename) | ||
file, err := os.ReadFile(configFile) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var c Config | ||
if err = yaml.Unmarshal(file, &c); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Ensure lower case formatting. | ||
for _, host := range c.Hosts { | ||
for _, i := range host.Interfaces { | ||
i.MACAddress = strings.ToLower(i.MACAddress) | ||
} | ||
} | ||
|
||
c.SourceDir = sourceDir | ||
c.DestinationDir = destinationDir | ||
|
||
return &c, nil | ||
} |
Oops, something went wrong.