Skip to content

Commit

Permalink
Merge pull request #2392 from mvo5/feature/native-grubenv
Browse files Browse the repository at this point in the history
partition: add support for native grubenv read/write and use it
  • Loading branch information
chipaca committed Jan 16, 2017
2 parents aba0955 + 5aad323 commit 08c0431
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 59 deletions.
1 change: 1 addition & 0 deletions debian/control
Expand Up @@ -13,6 +13,7 @@ Build-Depends: autoconf,
dh-systemd,
fakeroot,
gettext,
grub-common,
gnupg2,
golang-any (>=2:1.6) | golang-1.6,
indent,
Expand Down
46 changes: 13 additions & 33 deletions partition/grub.go
Expand Up @@ -20,22 +20,15 @@
package partition

import (
"fmt"
"os"
"path/filepath"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"

"github.com/mvo5/goconfigparser"
)

// var to make it testable
var (
grubEnvCmd = "/usr/bin/grub-editenv"
"github.com/snapcore/snapd/partition/grubenv"
)

type grub struct {
}
type grub struct{}

// newGrub create a new Grub bootloader object
func newGrub() Bootloader {
Expand Down Expand Up @@ -64,40 +57,27 @@ func (g *grub) envFile() string {
}

func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
out := map[string]string{}
out := make(map[string]string)

// Grub doesn't provide a get verb, so retrieve all values and
// search for the required variable ourselves.
output, err := runCommand(grubEnvCmd, g.envFile(), "list")
if err != nil {
return nil, err
}

cfg := goconfigparser.New()
cfg.AllowNoSectionHeader = true
if err := cfg.ReadString(output); err != nil {
env := grubenv.NewEnv(g.envFile())
if err := env.Load(); err != nil {
return nil, err
}

for _, name := range names {
v, err := cfg.Get("", name)
if err != nil {
return nil, err
}
out[name] = v
out[name] = env.Get(name)
}

return out, nil
}

func (g *grub) SetBootVars(values map[string]string) error {
// note that strings are not quoted since because
// runCommand does not use a shell and thus adding quotes
// stores them in the environment file (which is not desirable)
args := []string{grubEnvCmd, g.envFile(), "set"}
env := grubenv.NewEnv(g.envFile())
if err := env.Load(); err != nil && !os.IsNotExist(err) {
return err
}
for k, v := range values {
args = append(args, fmt.Sprintf("%s=%s", k, v))
env.Set(k, v)
}
_, err := runCommand(args...)
return err
return env.Save()
}
53 changes: 27 additions & 26 deletions partition/grub_test.go
Expand Up @@ -22,29 +22,40 @@ package partition
import (
"fmt"
"io/ioutil"
"os"
"sort"

"github.com/snapcore/snapd/dirs"
"path/filepath"

"github.com/mvo5/goconfigparser"
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
)

func mockGrubEditenvList(cmd ...string) (string, error) {
mockGrubEditenvOutput := fmt.Sprintf("%s=regular", bootmodeVar)
return mockGrubEditenvOutput, nil
func grubEnvPath() string {
return filepath.Join(dirs.GlobalRootDir, "boot/grub/grubenv")
}

func grubEditenvSet(c *C, key, value string) {
_, err := runCommand("/usr/bin/grub-editenv", grubEnvPath(), "set", fmt.Sprintf("%s=%s", key, value))
c.Assert(err, IsNil)
}

func mockGrubFile(c *C, newPath string, mode os.FileMode) {
err := ioutil.WriteFile(newPath, []byte(""), mode)
func grubEditenvGet(c *C, key string) string {
output, err := runCommand("/usr/bin/grub-editenv", grubEnvPath(), "list")
c.Assert(err, IsNil)
cfg := goconfigparser.New()
cfg.AllowNoSectionHeader = true
err = cfg.ReadString(output)
c.Assert(err, IsNil)
v, err := cfg.Get("", key)
c.Assert(err, IsNil)
return v
}

func (s *PartitionTestSuite) makeFakeGrubEnv(c *C) {
// these files just needs to exist
g := &grub{}
mockGrubFile(c, g.ConfigFile(), 0644)
mockGrubFile(c, g.envFile(), 0644)
err := ioutil.WriteFile(g.ConfigFile(), nil, 0644)
c.Assert(err, IsNil)
grubEditenvSet(c, "k", "v")
}

func (s *PartitionTestSuite) TestNewGrubNoGrubReturnsNil(c *C) {
Expand Down Expand Up @@ -72,7 +83,7 @@ func (s *PartitionTestSuite) TestGetBootloaderWithGrub(c *C) {

func (s *PartitionTestSuite) TestGetBootVer(c *C) {
s.makeFakeGrubEnv(c)
runCommand = mockGrubEditenvList
grubEditenvSet(c, bootmodeVar, "regular")

g := newGrub()
v, err := g.GetBootVars(bootmodeVar)
Expand All @@ -83,24 +94,14 @@ func (s *PartitionTestSuite) TestGetBootVer(c *C) {

func (s *PartitionTestSuite) TestSetBootVer(c *C) {
s.makeFakeGrubEnv(c)
cmds := [][]string{}
runCommand = func(cmd ...string) (string, error) {
cmds = append(cmds, cmd)
return "", nil
}

g := newGrub()
err := g.SetBootVars(map[string]string{
"k1": "v1",
"k2": "v2",
})
c.Assert(err, IsNil)
c.Check(cmds, HasLen, 1)
c.Check(cmds[0][0:3], DeepEquals, []string{
"/usr/bin/grub-editenv", g.(*grub).envFile(), "set",
})
// need to sort, its coming from a slice
kwargs := cmds[0][3:]
sort.Strings(kwargs)
c.Check(kwargs, DeepEquals, []string{"k1=v1", "k2=v2"})

c.Check(grubEditenvGet(c, "k1"), Equals, "v1")
c.Check(grubEditenvGet(c, "k2"), Equals, "v2")
}
123 changes: 123 additions & 0 deletions partition/grubenv/grubenv.go
@@ -0,0 +1,123 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package grubenv

import (
"bytes"
"fmt"
"io/ioutil"
"os"
)

// FIXME: support for escaping (embedded \n in grubenv) missing
type Env struct {
env map[string]string
ordering []string

path string
}

func NewEnv(path string) *Env {
return &Env{
env: make(map[string]string),
path: path,
}
}

func (g *Env) Get(name string) string {
return g.env[name]
}

func (g *Env) Set(key, value string) {
var contains = func(needle string, haystack []string) bool {
for _, k := range haystack {
if k == key {
return true
}
}
return false
}
if !contains(key, g.ordering) {
g.ordering = append(g.ordering, key)
}

g.env[key] = value
}

func (g *Env) Load() error {
buf, err := ioutil.ReadFile(g.path)
if err != nil {
return err
}
if len(buf) != 1024 {
return fmt.Errorf("grubenv %q must be exactly 1024 byte, got %d", g.path, len(buf))
}
if !bytes.HasPrefix(buf, []byte("# GRUB Environment Block\n")) {
return fmt.Errorf("cannot find grubenv header in %q", g.path)
}
rawEnv := bytes.Split(buf, []byte("\n"))
for _, env := range rawEnv[1:] {
l := bytes.SplitN(env, []byte("="), 2)
// be liberal in what you accept
if len(l) < 2 {
continue
}
k := string(l[0])
v := string(l[1])
g.env[k] = v
g.ordering = append(g.ordering, k)
}

return nil
}

func (g *Env) Save() error {
w := bytes.NewBuffer(nil)
w.Grow(1024)

fmt.Fprintf(w, "# GRUB Environment Block\n")
for _, k := range g.ordering {
if _, err := fmt.Fprintf(w, "%s=%s\n", k, g.env[k]); err != nil {
return err
}
}
if w.Len() > 1024 {
return fmt.Errorf("cannot write grubenv %q: bigger than 1024 bytes (%d)", g.path, w.Len())
}
content := w.Bytes()[:w.Cap()]
for i := w.Len(); i < len(content); i++ {
content[i] = '#'
}

// write in place to avoid the file moving on disk
// (thats what grubenv is also doing)
f, err := os.Create(g.path)
if err != nil {
return err
}
if _, err := f.Write(content); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}

return f.Close()
}
94 changes: 94 additions & 0 deletions partition/grubenv/grubenv_test.go
@@ -0,0 +1,94 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package grubenv_test

import (
"fmt"
"io/ioutil"
"path/filepath"
"testing"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/partition/grubenv"
)

// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }

type grubenvTestSuite struct {
envPath string
}

var _ = Suite(&grubenvTestSuite{})

func (g *grubenvTestSuite) SetUpTest(c *C) {
g.envPath = filepath.Join(c.MkDir(), "grubenv")
}

func (g *grubenvTestSuite) TestSet(c *C) {
env := grubenv.NewEnv(g.envPath)
c.Check(env, NotNil)

env.Set("key", "value")
c.Check(env.Get("key"), Equals, "value")
}

func (g *grubenvTestSuite) TestSave(c *C) {
env := grubenv.NewEnv(g.envPath)
c.Check(env, NotNil)

env.Set("key1", "value1")
env.Set("key2", "value2")
env.Set("key3", "value3")
env.Set("key4", "value4")
env.Set("key5", "value5")
env.Set("key6", "value6")
env.Set("key7", "value7")
// set "key1" again, ordering (position) does not change
env.Set("key1", "value1")

err := env.Save()
c.Assert(err, IsNil)

buf, err := ioutil.ReadFile(g.envPath)
c.Assert(err, IsNil)
c.Assert(buf, DeepEquals, []byte(`# GRUB Environment Block
key1=value1
key2=value2
key3=value3
key4=value4
key5=value5
key6=value6
key7=value7
###################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################`))
}

func (g *grubenvTestSuite) TestSaveOverflow(c *C) {
env := grubenv.NewEnv(g.envPath)
c.Check(env, NotNil)

for i := 0; i < 101; i++ {
env.Set(fmt.Sprintf("key%d", i), "foo")
}

err := env.Save()
c.Assert(err, ErrorMatches, `cannot write grubenv .*: bigger than 1024 bytes \(1026\)`)
}

0 comments on commit 08c0431

Please sign in to comment.