diff --git a/README.md b/README.md index 97faff8..0618a04 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ To replace the `/boot/occidentalis.txt` (as we want this feature for more boards * ☑ Set WiFi SSID/PSK * ☐ Set timezone * ☐ Set locale -* ☐ Set static IP (see hypriot/device-init#6) +* ☑ Set static IP +* ☑ Configure network interfaces * ☑ Pull some docker images * ☐ Install a list of DEB packages * ☑ Run a custom script from /boot @@ -25,8 +26,32 @@ The `device-init` tool reads the file `/boot/device-init.yaml` to initialize sev hostname: "black-pearl" ``` +### Network Settings + +```yaml +network: + interfaces: + eth0: + # sets a static IP + address: 192.168.13.37 + netmask: 255.255.255.0 + gateway: 192.168.13.1 + dnsnameservers: + - 8.8.8.8 + - 8.8.4.4 + dnssearch: + example.com + eth1: + # uses dhcp + wlan0: + ssid: "MyNetwork" + password: "secret_password" +``` + ### Wifi Settings +This option only allows creating wlan intefaces and setting SSID/PSK. For advanced configuration see [network configuration](#network-settings). + ```yaml wifi: interfaces: diff --git a/cmd/network.go b/cmd/network.go new file mode 100644 index 0000000..c567a44 --- /dev/null +++ b/cmd/network.go @@ -0,0 +1,199 @@ +package cmd + +import ( + "fmt" + "text/template" + "bytes" + "strings" + "os" + "path" + "io/ioutil" + "regexp" + "os/exec" + + "github.com/spf13/cobra" +) + +type Interface struct { + Name string + Address string + Netmask string + Gateway string + DNSNameservers []string + DNSSearch string + SSID string + Password string +} + +type NetworkConfig struct { + Interfaces map[string]Interface +} + +var networkCmd = &cobra.Command{ + Use: "network", + Short: "set network settings", + Long: `Set network settings. +Network configuration only possible via config file.`, + Run: func(cmd *cobra.Command, args []string) { + if config.IsSet("network") { + setNetwork() + } else { + cmd.Help() + } + }, +} + +func setNetwork() { + if config.IsSet("network") { + var networkConfig NetworkConfig + err := config.UnmarshalKey("network", &networkConfig) + if err != nil { + fmt.Println("Could not unmarshal Network configuration") + } + configureNetwork(networkConfig) + } +} + +func configureNetwork(networkConfig NetworkConfig) { + for interfaceName, interfaceConfig := range networkConfig.Interfaces { + interfaceConfig.Name = interfaceName + + if interfaceConfig.Password != "" && interfaceConfig.SSID != ""{ + interfaceConfig.Password = createEncryptedPsk([]byte(interfaceConfig.Password), []byte(interfaceConfig.SSID)) + } + + interfaceString := generateInterfaceConfig(interfaceConfig) + applyInterfaceConfig(interfaceName, interfaceString) + } +} + +func generateInterfaceConfig(interfaceConfig Interface) string { + var interfaceString string + + const interfaceTemlate = `# interface configuration generated by device-init +allow-hotplug {{.Name}} +auto {{.Name}} +iface {{.Name}} inet {{if .Address}}static{{else}}dhcp{{end}} + {{if .Address}}address {{.Address}}{{end}} + {{if .Netmask}}netmask {{.Netmask}}{{end}} + {{if .Gateway}}gateway {{.Gateway}}{{end}} + {{if .DNSNameservers}}dns-nameservers {{range $key, $value := .DNSNameservers}}{{$value}} {{end}}{{end}} + {{if .DNSSearch}}dns-search {{.DNSSearch}}{{end}} + {{if .SSID}}wpa-ssid {{.SSID}}{{end}} + {{if .Password}}wpa-psk {{.Password}}{{end}} + {{if .Address}}pre-up ip addr flush dev {{.Name}}{{end}} +` + t := template.Must(template.New("config").Parse(interfaceTemlate)) + var buffer bytes.Buffer + err := t.Execute(&buffer, interfaceConfig) + if err != nil { + fmt.Println("Error writing configuration:", err) + } + + interfaceString = buffer.String() + interfaceString = strings.Replace(interfaceString, " \n", "", -1) + + return interfaceString +} + +func applyInterfaceConfig(interfaceName string, interfaceConfig string) { + err := os.MkdirAll(networkInterfacesPath, 0755) + if err != nil { + fmt.Println("Could not create path: ", networkInterfacesPath) + } + + configFilePath := path.Join(networkInterfacesPath, interfaceName) + if _, err := os.Stat(configFilePath); err == nil { + filepath, filename := path.Dir(configFilePath), path.Base(configFilePath) + backupFile := "." + filename + ".backup" + backupPath := path.Join(filepath, backupFile) + err = os.Rename(configFilePath, backupPath) + if err != nil { + fmt.Println("Could not backup file ", backupPath, ": ", err) + } + } + + f, err := os.Create(configFilePath) + defer f.Close() + if err != nil { + fmt.Println("Could not create file: "+configFilePath+": ", err) + } + + err = ioutil.WriteFile(configFilePath, []byte(interfaceConfig), 0644) + if err != nil { + panic(err) + } + + // restart an existing interface + if interfaceExists(interfaceName) { + output, err := exec.Command("/sbin/ifdown", interfaceName).CombinedOutput() + if err != nil { + message := fmt.Sprintf("Could not bring the interface down %s: %s ", interfaceName, err) + fmt.Println(message) + } + fmt.Println(string(output)[:]) + } + + if interfaceExistsAndIsDown(interfaceName) { + output, err := exec.Command("/sbin/ifup", interfaceName).CombinedOutput() + if err != nil { + message := fmt.Sprintf("Could not bring up interface %s: %s", interfaceName, err) + fmt.Println(message) + } + fmt.Println(string(output)[:]) + } + + // try to bring the interface up once more but bring it down before + if interfaceExistsAndIsDown(interfaceName) { + output, err := exec.Command("/sbin/ifdown", interfaceName).CombinedOutput() + if err != nil { + message := fmt.Sprintf("Could not bring the interface down %s: %s ", interfaceName, err) + fmt.Println(message) + } + fmt.Println(string(output)[:]) + output, err = exec.Command("/sbin/ifup", interfaceName).CombinedOutput() + if err != nil { + message := fmt.Sprintf("Could still not bring up interface %s: %s", interfaceName, err) + fmt.Println(message) + } + } +} + + +func init() { + RootCmd.AddCommand(networkCmd) +} + + +func interfaceExistsAndIsDown(interfaceName string) bool { + stats := getInterfaceStats() + for _, line := range stats { + interfaceExists, _ := regexp.MatchString(interfaceName, line) + interfaceIsDown, _ := regexp.MatchString("state DOWN", line) + if interfaceExists && interfaceIsDown { + return true + } + } + return false +} + + +func interfaceExists(interfaceName string) bool { + stats := getInterfaceStats() + for _, line := range stats { + interfaceExists, _ := regexp.MatchString(interfaceName, line) + if interfaceExists { + return true + } + } + return false +} + + +func getInterfaceStats() []string { + output, err := exec.Command("ip", "link").Output() + if err != nil { + fmt.Println("Could not run 'ip link'", err) + } + return strings.Split(string(output), "\n") +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index f179a75..e66eda7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,7 @@ func setAllCommands() { if err := config.ReadInConfig(); err == nil { setHostname() setWifi() + setNetwork() dockerPreloadImages() manageClusterLab() runCommands() @@ -83,5 +84,4 @@ func initConfig() { fmt.Println("Using config file:", config.ConfigFileUsed()) } } - } diff --git a/cmd/wifi_set.go b/cmd/wifi_set.go index 01bb5ea..9163691 100644 --- a/cmd/wifi_set.go +++ b/cmd/wifi_set.go @@ -23,15 +23,9 @@ package cmd import ( "crypto/sha1" "encoding/hex" - "fmt" + "github.com/spf13/cobra" "golang.org/x/crypto/pbkdf2" - "os" - "os/exec" - "path" - "regexp" - "strings" - "text/template" ) // setCmd represents the set command @@ -45,18 +39,8 @@ var setWifiCmd = &cobra.Command{ } func setWifi() { - var err error - readWifiConfig() - const interfaceTemlate = `allow-hotplug {{.Name}} - -auto {{.Name}} -iface {{.Name}} inet dhcp - wpa-ssid {{.Ssid}} - wpa-psk {{.Psk}} -` - // if we have command line parameters only add those to our wifi configuration if cmdInterfaceName != "" && cmdSsid != " " && cmdPassword != "" { for key := range myWifiConfig.Interfaces { @@ -67,69 +51,13 @@ iface {{.Name}} inet dhcp } for interfaceName, interfaceCredentials := range myWifiConfig.Interfaces { - - type templateVariables struct { - Name, Ssid, Psk string - } - - variables := templateVariables{ + variables := Interface{ Name: interfaceName, - Ssid: interfaceCredentials.Ssid, - Psk: createEncryptedPsk([]byte(interfaceCredentials.Password), []byte(interfaceCredentials.Ssid)), - } - - err = os.MkdirAll(networkInterfacesPath, 0755) - if err != nil { - fmt.Println("Could not create path: ", networkInterfacesPath) - } - - configFilePath := path.Join(networkInterfacesPath, interfaceName) - if _, err := os.Stat(configFilePath); err == nil { - filepath, filename := path.Dir(configFilePath), path.Base(configFilePath) - backupFile := "." + filename + ".backup" - backupPath := path.Join(filepath, backupFile) - err = os.Rename(configFilePath, backupPath) - if err != nil { - fmt.Println("Could not backup file ", backupPath, ": ", err) - } - } - - f, err := os.Create(configFilePath) - defer f.Close() - if err != nil { - fmt.Println("Could not create file: "+configFilePath+": ", err) - } - - t := template.Must(template.New("config").Parse(interfaceTemlate)) - - err = t.Execute(f, variables) - if err != nil { - fmt.Println("Error writing configuration:", err) - } - - if interfaceExistsAndIsDown(interfaceName) { - output, err := exec.Command("/sbin/ifup", interfaceName).CombinedOutput() - if err != nil { - message := fmt.Sprintf("Could not bring up interface %s: %s", interfaceName, err) - fmt.Println(message) - } - fmt.Println(string(output)[:]) - } - - // try to bring the interface up once more but bring it down before - if interfaceExistsAndIsDown(interfaceName) { - output, err := exec.Command("/sbin/ifdown", interfaceName).CombinedOutput() - if err != nil { - message := fmt.Sprintf("Could not bring the interface down %s: %s ", interfaceName, err) - fmt.Println(message) - } - fmt.Println(string(output)[:]) - output, err = exec.Command("/sbin/ifup", interfaceName).CombinedOutput() - if err != nil { - message := fmt.Sprintf("Could still not bring up interface %s: %s", interfaceName, err) - fmt.Println(message) - } + SSID: interfaceCredentials.Ssid, + Password: createEncryptedPsk([]byte(interfaceCredentials.Password), []byte(interfaceCredentials.Ssid)), } + interfaceString := generateInterfaceConfig(variables) + applyInterfaceConfig(interfaceName, interfaceString) } } @@ -152,19 +80,3 @@ func createEncryptedPsk(password, salt []byte) string { result := pbkdf2.Key(password, salt, 4096, 32, sha1.New) return hex.EncodeToString(result) } - -func interfaceExistsAndIsDown(interfaceName string) bool { - output, err := exec.Command("ip", "link").Output() - if err != nil { - fmt.Println("Could not run 'ip link'", err) - } - lines := strings.Split(string(output), "\n") - for _, line := range lines { - interfaceExists, _ := regexp.MatchString(interfaceName, line) - interfaceIsDown, _ := regexp.MatchString("state DOWN", line) - if interfaceExists && interfaceIsDown { - return true - } - } - return false -} diff --git a/specs/device-init_spec.rb b/specs/device-init_spec.rb index 5f23689..f44baa4 100644 --- a/specs/device-init_spec.rb +++ b/specs/device-init_spec.rb @@ -188,6 +188,115 @@ end end + + context "network" do + + context "with config-file" do + let(:config_static_interface) { File.read(File.join(File.dirname(__FILE__), 'testdata', 'static_interface.yaml')) } + let(:config_mixed_interfaces) { File.read(File.join(File.dirname(__FILE__), 'testdata', 'mixed_interface.yaml')) } + let(:config_no_network) { File.read(File.join(File.dirname(__FILE__), 'testdata', 'no_wifi_interface.yaml')) } + + context "sets config" do + before(:each) do + expect(command('rm -Rf /etc/network/interfaces.d').exit_status).to be(0) + end + + it "creates configuration for one static interface entry" do + status = command(%Q(echo -n '#{config_static_interface}' > /boot/device-init.yaml)).exit_status + expect(status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(false) + + device_init_cmd_result = command('device-init --config') + expect(device_init_cmd_result.exit_status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(true) + expect(network_interface_dir.directory?).to be(true) + + interface_config_file = file('/etc/network/interfaces.d/eth0') + expect(interface_config_file.exists?).to be(true) + expect(interface_config_file).to contain('allow-hotplug eth0') + expect(interface_config_file).to contain('auto eth0') + expect(interface_config_file).to contain('iface eth0 inet static') + expect(interface_config_file).to contain('address 192.168.1.42') + expect(interface_config_file).to contain('gateway 192.168.1.0') + expect(interface_config_file).to contain('netmask 255.255.255.0') + expect(interface_config_file).to contain('dns-nameservers 8.8.8.8 8.8.4.4') + expect(interface_config_file).to contain('dns-search example.com') + end + + it "creates configuration for multiple mixed (static/dhcp) interface entries" do + status = command(%Q(echo -n '#{config_mixed_interfaces}' > /boot/device-init.yaml)).exit_status + expect(status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(false) + + device_init_cmd_result = command('device-init --config') + expect(device_init_cmd_result.exit_status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(true) + expect(network_interface_dir.directory?).to be(true) + + interface_config_file = file('/etc/network/interfaces.d/eth0') + expect(interface_config_file.exists?).to be(true) + expect(interface_config_file).to contain('allow-hotplug eth0') + expect(interface_config_file).to contain('auto eth0') + expect(interface_config_file).to contain('iface eth0 inet static') + expect(interface_config_file).to contain('address 192.168.1.42') + expect(interface_config_file).to contain('gateway 192.168.1.0') + expect(interface_config_file).to contain('netmask 255.255.255.0') + expect(interface_config_file).to contain('dns-nameservers 8.8.8.8 8.8.4.4') + expect(interface_config_file).to contain('dns-search example.com') + + interface_config_file = file('/etc/network/interfaces.d/eth1') + expect(interface_config_file.exists?).to be(true) + expect(interface_config_file).to contain('allow-hotplug eth1') + expect(interface_config_file).to contain('auto eth1') + expect(interface_config_file).to contain('iface eth1 inet dhcp') + + interface_config_file = file('/etc/network/interfaces.d/wlan0') + expect(interface_config_file.exists?).to be(true) + expect(interface_config_file).to contain('allow-hotplug wlan0') + expect(interface_config_file).to contain('auto wlan0') + expect(interface_config_file).to contain('iface wlan0 inet static') + expect(interface_config_file).to contain('address 192.168.1.123') + expect(interface_config_file).to contain('gateway 192.168.1.0') + expect(interface_config_file).to contain('netmask 255.255.255.0') + expect(interface_config_file).to contain('dns-nameservers 8.8.8.8 8.8.4.4') + expect(interface_config_file).to contain('dns-search example.com') + expect(interface_config_file).to contain('wpa-ssid MyNetwork') + expect(interface_config_file).to contain('wpa-psk a53576661368249ebfa26f8828669ad0e6f0523154b55404b33a21ca1242b845') + + interface_config_file = file('/etc/network/interfaces.d/wlan1') + expect(interface_config_file.exists?).to be(true) + expect(interface_config_file).to contain('allow-hotplug wlan1') + expect(interface_config_file).to contain('auto wlan1') + expect(interface_config_file).to contain('iface wlan1 inet dhcp') + expect(interface_config_file).to contain('wpa-ssid MySecondNetwork') + expect(interface_config_file).to contain('wpa-psk 32919a0369631b758391d00e2aaaf66e6ab61b61949cc853c45410fbf4910442') + end + + it "creates no configuration if there is no 'network' key in device-init.yaml" do + status = command(%Q(echo -n '#{config_no_network}' > /boot/device-init.yaml)).exit_status + expect(status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(false) + + device_init_cmd_result = command('device-init --config') + expect(device_init_cmd_result.exit_status).to be(0) + + network_interface_dir = file('/etc/network/interfaces.d/') + expect(network_interface_dir.exists?).to be(false) + end + end + + end + end context "docker" do context "preload-images" do diff --git a/specs/testdata/mixed_interface.yaml b/specs/testdata/mixed_interface.yaml new file mode 100644 index 0000000..5e2433d --- /dev/null +++ b/specs/testdata/mixed_interface.yaml @@ -0,0 +1,24 @@ +network: + interfaces: + eth0: + address: 192.168.1.42 + gateway: 192.168.1.0 + netmask: 255.255.255.0 + dnsnameservers: + - 8.8.8.8 + - 8.8.4.4 + dnssearch: example.com + eth1: + wlan0: + address: 192.168.1.123 + gateway: 192.168.1.0 + netmask: 255.255.255.0 + dnsnameservers: + - 8.8.8.8 + - 8.8.4.4 + dnssearch: example.com + ssid: MyNetwork + password: my_secret_password + wlan1: + ssid: MySecondNetwork + password: another_secret_password \ No newline at end of file diff --git a/specs/testdata/static_interface.yaml b/specs/testdata/static_interface.yaml new file mode 100644 index 0000000..f634dfe --- /dev/null +++ b/specs/testdata/static_interface.yaml @@ -0,0 +1,10 @@ +network: + interfaces: + eth0: + address: 192.168.1.42 + gateway: 192.168.1.0 + netmask: 255.255.255.0 + dnsnameservers: + - 8.8.8.8 + - 8.8.4.4 + dnssearch: example.com \ No newline at end of file