From d7e0591576fb8d354413d0f6c9ad89a893880e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=B6hmke?= Date: Fri, 29 Nov 2019 19:53:17 +0100 Subject: [PATCH 1/3] added network config --- .gitignore | 3 +- go.mod | 5 ++ go.sum | 13 ++++ network.go | 171 +++++++++++++++++++++++++++++++++++++++++++++ network_test.go | 116 ++++++++++++++++++++++++++++++ resources/build.sh | 6 +- 6 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 network.go create mode 100644 network_test.go diff --git a/.gitignore b/.gitignore index 62c8935..ec4ecb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/ \ No newline at end of file +.idea/ +test_* \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c61013 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitlab.com/bboehmke/raspi-alpine-builder + +go 1.13 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a65f495 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/kataras/iris v11.1.1+incompatible/go.mod h1:ki9XPua5SyAJbIxDdsssxevgGrbpBmmvoQmo/A0IodY= +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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/network.go b/network.go new file mode 100644 index 0000000..5553f33 --- /dev/null +++ b/network.go @@ -0,0 +1,171 @@ +package alpine_builder + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strings" +) + +// network configuration +var networkConfig = "/data/etc/network/interfaces" +var networkInterface = "eth0" + +// regular expressions +var reSectionStart = regexp.MustCompile("^(iface|mapping|auto|allow-|source)") +var reIface = regexp.MustCompile("^iface (\\w+) inet (\\w+)") +var reStaticAddress = regexp.MustCompile("^(\\s*)address ([0-9.]+)") +var reStaticNetmask = regexp.MustCompile("^(\\s*)netmask ([0-9.]+)") +var reStaticGateway = regexp.MustCompile("^(\\s*)gateway ([0-9.]+)") + +// NetworkInfo represents the actual network configuration +type NetworkInfo struct { + IsStatic bool + Address string + Netmask string + Gateway string +} + +// GetNetworkInfo from config file +func GetNetworkInfo() (*NetworkInfo, error) { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return nil, fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var info *NetworkInfo + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + info = &NetworkInfo{ + IsStatic: infLine[2] == "static", + } + } + } else { + // info set -> stop here + if info != nil { + break + } + } + + } else if info != nil { + // interface config + address := reStaticAddress.FindStringSubmatch(line) + if address != nil { + info.Address = address[2] + } + netmask := reStaticNetmask.FindStringSubmatch(line) + if netmask != nil { + info.Netmask = netmask[2] + } + gateway := reStaticGateway.FindStringSubmatch(line) + if gateway != nil { + info.Gateway = gateway[2] + } + } + } + if info != nil { + return info, nil + } + return nil, errors.New("invalid network config") +} + +// NetworkEnableDHCP configures the network to use DHCP client +func NetworkEnableDHCP() error { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var buffer bytes.Buffer + var infFound bool + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + infFound = true + buffer.WriteString(fmt.Sprintf("iface %s inet dhcp\n", networkInterface)) + continue + } + } else { + if infFound { + infFound = false + } + } + buffer.WriteString(line + "\n") + + } else if !infFound { + buffer.WriteString(line + "\n") + } + } + + // write config file + return ioutil.WriteFile(networkConfig, buffer.Bytes(), os.ModePerm) +} + +// NetworkSetStatic IP configuration +func NetworkSetStatic(address, netmask, gateway string) error { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var buffer bytes.Buffer + var infFound bool + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + infFound = true + buffer.WriteString(fmt.Sprintf("iface %s inet static\n", networkInterface)) + buffer.WriteString(fmt.Sprintf(" address %s\n", address)) + buffer.WriteString(fmt.Sprintf(" netmask %s\n", netmask)) + buffer.WriteString(fmt.Sprintf(" gateway %s\n", gateway)) + continue + } + } else { + if infFound { + infFound = false + } + } + buffer.WriteString(line + "\n") + + } else if !infFound { + buffer.WriteString(line + "\n") + } + } + + // write config file + return ioutil.WriteFile(networkConfig, buffer.Bytes(), os.ModePerm) +} diff --git a/network_test.go b/network_test.go new file mode 100644 index 0000000..ade6384 --- /dev/null +++ b/network_test.go @@ -0,0 +1,116 @@ +package alpine_builder + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + networkConfig = "test_interfaces" +} + +func TestGetNetworkInfo(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + // invalid config file + ass.NoError(ioutil.WriteFile(networkConfig, []byte(""), os.ModePerm)) + _, err = GetNetworkInfo() + ass.EqualError(err, "invalid network config") + + // dynamic config + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp`), os.ModePerm)) + info, err := GetNetworkInfo() + ass.NoError(err) + ass.False(info.IsStatic) + + // static config + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1`), os.ModePerm)) + info, err = GetNetworkInfo() + ass.NoError(err) + ass.True(info.IsStatic) + ass.Equal("1.2.3.4", info.Address) + ass.Equal("255.255.255.0", info.Netmask) + ass.Equal("4.3.2.1", info.Gateway) + + _ = os.Remove(networkConfig) +} + +func TestNetworkEnableDHCP(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1`), os.ModePerm)) + + ass.NoError(NetworkEnableDHCP()) + + data, err := ioutil.ReadFile(networkConfig) + ass.NoError(err) + + ass.Equal(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp +`, string(data)) + + _ = os.Remove(networkConfig) +} + +func TestNetworkSetStatic(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp`), os.ModePerm)) + + ass.NoError(NetworkSetStatic("1.2.3.4", "255.255.255.0", "4.3.2.1")) + + data, err := ioutil.ReadFile(networkConfig) + ass.NoError(err) + + ass.Equal(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1 +`, string(data)) + + _ = os.Remove(networkConfig) +} diff --git a/resources/build.sh b/resources/build.sh index 9a1ed67..1664dba 100755 --- a/resources/build.sh +++ b/resources/build.sh @@ -99,7 +99,7 @@ EOF # prepare network chroot_exec rc-update add networking default -ln -fs /data/etc/interfaces ${ROOTFS_PATH}/etc/network/interfaces +ln -fs /data/etc/network/interfaces ${ROOTFS_PATH}/etc/network/interfaces # run local before network -> local brings up the interface sed -i '/^\tneed/ s/$/ local/' ${ROOTFS_PATH}/etc/init.d/networking @@ -200,8 +200,8 @@ root_pw=\$(mkpasswd -m sha-512 -s "${DEFAULT_ROOT_PASSWORD}") echo "root:\${root_pw}:0:0:::::" > /data/etc/shadow # interface -if [ ! -f /data/etc/interfaces ]; then -cat > /data/etc/interfaces < /data/etc/network/interfaces < Date: Fri, 29 Nov 2019 22:12:38 +0100 Subject: [PATCH 2/3] added system config and actions --- go.mod | 5 ++- go.sum | 5 +-- system.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ system_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 system.go create mode 100644 system_test.go diff --git a/go.mod b/go.mod index 7c61013..db4dfa7 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module gitlab.com/bboehmke/raspi-alpine-builder go 1.13 -require github.com/stretchr/testify v1.4.0 +require ( + github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 + github.com/stretchr/testify v1.4.0 +) diff --git a/go.sum b/go.sum index a65f495..eb9f130 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,14 @@ +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 h1:rs0kDBt2zF4/CM9rO5/iH+U22jnTygPlqWgX55Ufcxg= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/kataras/iris v11.1.1+incompatible/go.mod h1:ki9XPua5SyAJbIxDdsssxevgGrbpBmmvoQmo/A0IodY= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/system.go b/system.go new file mode 100644 index 0000000..ca32547 --- /dev/null +++ b/system.go @@ -0,0 +1,84 @@ +package alpine_builder + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" +) + +// system configurations +var systemShadow = "/data/etc/shadow" +var systemDropbearConfig = "/data/etc/dropbear/dropbear.conf" +var systemDropbearRestart = "rc-service dropbear start" +var systemShutdown = "poweroff" +var systemReboot = "reboot" + +// SystemSetRootPassword update shadow file +func SystemSetRootPassword(password string) error { + crypter := crypt.SHA256.New() + + // generate password line + hash, err := crypter.Generate([]byte(password), nil) + if err != nil { + return fmt.Errorf("failed to generate hash: %w", err) + } + line := fmt.Sprintf("root:%s:0:0:::::", hash) + + // write shadow file + return ioutil.WriteFile(systemShadow, []byte(line), os.ModePerm) +} + +// SystemEnableSSH server +func SystemEnableSSH() error { + err := ioutil.WriteFile(systemDropbearConfig, []byte("DROPBEAR_OPTS=\"\""), os.ModePerm) + if err != nil { + return fmt.Errorf("failed to write ssh config: %w", err) + } + + cmdSplit := strings.Split(systemDropbearRestart, " ") + cmd := exec.Command(cmdSplit[0], cmdSplit[1:]...) + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to restart ssh server: %w", err) + } + return nil +} + +// SystemDisableSSH server +func SystemDisableSSH() error { + err := ioutil.WriteFile(systemDropbearConfig, []byte("DROPBEAR_OPTS=\"-p 127.0.0.1:22\""), os.ModePerm) + if err != nil { + return fmt.Errorf("failed to write ssh config: %w", err) + } + + cmdSplit := strings.Split(systemDropbearRestart, " ") + cmd := exec.Command(cmdSplit[0], cmdSplit[1:]...) + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to restart ssh server: %w", err) + } + return nil +} + +func SystemShutdown() error { + cmd := exec.Command(systemShutdown) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to start system shutdown: %w", err) + } + return nil +} + +func SystemReboot() error { + cmd := exec.Command(systemReboot) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to start system reboot: %w", err) + } + return nil +} diff --git a/system_test.go b/system_test.go new file mode 100644 index 0000000..79b4d64 --- /dev/null +++ b/system_test.go @@ -0,0 +1,75 @@ +package alpine_builder + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" + "github.com/stretchr/testify/assert" +) + +func init() { + systemShadow = "test_shadow" + systemDropbearConfig = "test_dropbear" + systemDropbearRestart = "restart_dropbear_command" + systemReboot = "reboot_command" + systemShutdown = "shutdown_command" +} + +func TestSystemSetRootPassword(t *testing.T) { + ass := assert.New(t) + + ass.NoError(SystemSetRootPassword("password")) + + data, err := ioutil.ReadFile(systemShadow) + ass.NoError(err) + hash := strings.Split(string(data), ":")[1] + + crypter := crypt.SHA256.New() + ass.NoError(crypter.Verify(hash, []byte("password"))) + + _ = os.Remove(systemShadow) +} + +func TestSystemEnableSSH(t *testing.T) { + ass := assert.New(t) + + ass.EqualError(SystemEnableSSH(), "failed to restart ssh server: exec: \"restart_dropbear_command\": executable file not found in $PATH") + + data, err := ioutil.ReadFile(systemDropbearConfig) + ass.NoError(err) + + ass.Equal("DROPBEAR_OPTS=\"\"", string(data)) + + _ = os.Remove(systemDropbearConfig) +} + +func TestSystemDisableSSH(t *testing.T) { + ass := assert.New(t) + + ass.EqualError(SystemDisableSSH(), "failed to restart ssh server: exec: \"restart_dropbear_command\": executable file not found in $PATH") + + data, err := ioutil.ReadFile(systemDropbearConfig) + ass.NoError(err) + + ass.Equal("DROPBEAR_OPTS=\"-p 127.0.0.1:22\"", string(data)) + + _ = os.Remove(systemDropbearConfig) +} + +func TestSystemReboot(t *testing.T) { + ass := assert.New(t) + + ass.EqualError(SystemReboot(), + "failed to start system reboot: exec: \"reboot_command\": executable file not found in $PATH") +} + +func TestSystemShutdown(t *testing.T) { + ass := assert.New(t) + + ass.EqualError(SystemShutdown(), + "failed to start system shutdown: exec: \"shutdown_command\": executable file not found in $PATH") +} From cecb06c7ac4d75c4c847ef1d92d04aacb38d681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=B6hmke?= Date: Sun, 1 Dec 2019 21:20:32 +0100 Subject: [PATCH 3/3] added update & uboot functions --- resources/scripts/ab_active | 2 +- resources/scripts/ab_flash | 2 +- resources/uboot.c | 2 +- system.go | 2 + update.go | 143 ++++++++++++++++++++++++++++ update_test.go | 179 ++++++++++++++++++++++++++++++++++++ 6 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 update.go create mode 100644 update_test.go diff --git a/resources/scripts/ab_active b/resources/scripts/ab_active index f6c66ec..4350e34 100755 --- a/resources/scripts/ab_active +++ b/resources/scripts/ab_active @@ -1,4 +1,4 @@ - #!/bin/bash +#!/bin/bash set -e # get current partition index diff --git a/resources/scripts/ab_flash b/resources/scripts/ab_flash index def73a3..c44e0c5 100755 --- a/resources/scripts/ab_flash +++ b/resources/scripts/ab_flash @@ -1,4 +1,4 @@ - #!/bin/bash +#!/bin/bash set -e image_file=$1 diff --git a/resources/uboot.c b/resources/uboot.c index d58e8d5..bc00625 100644 --- a/resources/uboot.c +++ b/resources/uboot.c @@ -71,7 +71,7 @@ int main(int argc, char* argv[]) { data[1] = 0; // boot partition - data[2] = 2; // A=2, B=3 + data[2] = 2; // A=1, B=2 } // handle commands diff --git a/system.go b/system.go index ca32547..4b52167 100644 --- a/system.go +++ b/system.go @@ -65,6 +65,7 @@ func SystemDisableSSH() error { return nil } +// SystemShutdown start shutdown of system func SystemShutdown() error { cmd := exec.Command(systemShutdown) err := cmd.Run() @@ -74,6 +75,7 @@ func SystemShutdown() error { return nil } +// SystemReboot start reboot of system func SystemReboot() error { cmd := exec.Command(systemReboot) err := cmd.Run() diff --git a/update.go b/update.go new file mode 100644 index 0000000..fe0316a --- /dev/null +++ b/update.go @@ -0,0 +1,143 @@ +package alpine_builder + +import ( + "compress/gzip" + "encoding/binary" + "errors" + "fmt" + "hash/crc32" + "io" + "io/ioutil" + "log" + "os" + "os/exec" +) + +var ubootFile = "/uboot/uboot.dat" +var ubootRemountRW = "mount -o remount,rw /uboot" +var ubootRemountRO = "mount -o remount,ro /uboot" + +var rootPartitionA = "/dev/mmcblk0p1" +var rootPartitionB = "/dev/mmcblk0p2" + +// loadUbootDat file to byte array +func loadUbootDat() ([]byte, error) { + defaultData := make([]byte, 1024) + defaultData[0] = 1 // file version + defaultData[1] = 0 // boot counter + defaultData[2] = 1 // boot partition A=1, B=2 + + data, err := ioutil.ReadFile(ubootFile) + if err != nil { + return defaultData, fmt.Errorf("failed to open file: %w", err) + } + + // invalid dat file? + if len(data) < 1024 { + return defaultData, errors.New("invalid dat file -> fallback to defaults") + } + + crc := binary.LittleEndian.Uint32(data[1020:]) + bla := crc32.ChecksumIEEE(data[:1020]) + if crc != bla { + return defaultData, errors.New("invalid crc -> fallback to defaults") + } + + return data, nil +} + +// saveUbootDat from byte array +func saveUbootDat(data []byte) error { + // update crc + binary.LittleEndian.PutUint32(data[1020:], + crc32.ChecksumIEEE(data[:1020])) + + // remount boot partition - writable + cmd := exec.Command(ubootRemountRW) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to remount RW: %w", err) + } + + // update uboot dat file + err = ioutil.WriteFile(ubootFile, data[:1024], os.ModePerm) + if err != nil { + return fmt.Errorf("failed write uboot dat: %w", err) + } + + // remount boot partition - readonly + cmd = exec.Command(ubootRemountRO) + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to remount RO: %w", err) + } + return nil +} + +// UBootResetCounter to 0 +func UBootResetCounter() error { + data, _ := loadUbootDat() + data[1] = 0 + return saveUbootDat(data) +} + +// UBootActive returns the active partition. A=1, B=2 +func UBootActive() uint8 { + data, _ := loadUbootDat() + return data[2] +} + +// UBootSetActive sets the active partition. A=1, B=2 +func UBootSetActive(active uint8) error { + data, _ := loadUbootDat() + if active == 1 { + data[2] = 1 + } else { + data[2] = 2 + } + return saveUbootDat(data) +} + +// UpdateSystem with the given image +func UpdateSystem(image string) error { + data, _ := loadUbootDat() + rootPart := rootPartitionA + if data[2] == 1 { + rootPart = rootPartitionB + } + + // open image file + inFile, err := os.Open(image) + if err != nil { + log.Fatal(err) + } + defer inFile.Close() + + // decompress image + inDecompress, err := gzip.NewReader(inFile) + if err != nil { + log.Fatal(err) + } + defer inDecompress.Close() + + // open root partition + out, err := os.OpenFile(rootPart, + os.O_WRONLY|os.O_TRUNC|os.O_SYNC, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer out.Close() + + // write update + _, err = io.Copy(out, inDecompress) + if err != nil { + log.Fatal(err) + } + + // switch active partition + if data[2] == 1 { + return UBootSetActive(2) + } else { + return UBootSetActive(1) + } +} diff --git a/update_test.go b/update_test.go new file mode 100644 index 0000000..19aad20 --- /dev/null +++ b/update_test.go @@ -0,0 +1,179 @@ +package alpine_builder + +import ( + "bytes" + "compress/gzip" + "crypto/rand" + "encoding/binary" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + ubootFile = "test_uboot" + ubootRemountRW = "true" + ubootRemountRO = "true" + rootPartitionA = "test_rootA" + rootPartitionB = "test_rootB" +} + +func TestLoadUbootDat(t *testing.T) { + ass := assert.New(t) + + // file missing + data, err := loadUbootDat() + ass.EqualError(err, "failed to open file: open test_uboot: no such file or directory") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with invalid data + ass.NoError(ioutil.WriteFile(ubootFile, []byte{}, os.ModePerm)) + + data, err = loadUbootDat() + ass.EqualError(err, "invalid dat file -> fallback to defaults") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with invalid CRC + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + data, err = loadUbootDat() + ass.EqualError(err, "invalid crc -> fallback to defaults") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with valid CRC + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + data, err = loadUbootDat() + ass.NoError(err) + ass.Equal(testData, data) + + _ = os.Remove(ubootFile) +} + +func TestSaveUbootDat(t *testing.T) { + ass := assert.New(t) + + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + ass.NoError(saveUbootDat(testData)) + + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(testData, data) + + _ = os.Remove(ubootFile) +} + +func TestUBootResetCounter(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.NoError(UBootResetCounter()) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Zero(data[1]) + + _ = os.Remove(ubootFile) +} + +func TestUBootActive(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.Equal(uint8(2), UBootActive()) + + _ = os.Remove(ubootFile) +} + +func TestUBootSetActive(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.NoError(UBootSetActive(1)) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(uint8(1), data[2]) + + _ = os.Remove(ubootFile) +} + +func TestUpdateSystem(t *testing.T) { + ass := assert.New(t) + + // test uboot file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + // generate test image content + size := int64(1024 * 1024 * 5) + testImgData := make([]byte, size) + buffer := bytes.NewBuffer(testImgData) + _, err := io.CopyN(buffer, rand.Reader, size) + ass.NoError(err) + + // write compressed image file + file, err := os.Create("test_image.gz") + ass.NoError(err) + gzipWriter := gzip.NewWriter(file) + _, err = gzipWriter.Write(testImgData) + ass.NoError(err) + ass.NoError(gzipWriter.Close()) + ass.NoError(file.Close()) + + ass.NoError(ioutil.WriteFile(rootPartitionA, nil, os.ModePerm)) + ass.NoError(UpdateSystem("test_image.gz")) + + // check if image was written + data, err := ioutil.ReadFile(rootPartitionA) + ass.NoError(err) + ass.Equal(testImgData, data) + + // check if uboot dat was updated + data, err = ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(uint8(1), data[2]) + + _ = os.Remove("test_image.gz") + _ = os.Remove(ubootFile) + _ = os.Remove(rootPartitionA) + _ = os.Remove(rootPartitionB) +}