diff --git a/osprepare/distro.go b/osprepare/distro.go new file mode 100644 index 000000000..f4f7f4d11 --- /dev/null +++ b/osprepare/distro.go @@ -0,0 +1,268 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +const ( + aptSourcesList = "/etc/apt/sources.list" + aptSourcesListD = "/etc/apt/sources.list.d" +) + +// aptSourcesFile abstracts a source.list file into +// a list of aptSource structs and the file where +// those definitions are located. +// Each apt source list may have multiple sources +type aptSourcesFile struct { + Sources []*aptSource + Path string +} + +// aptSource struct contains all the fields +// in an apt line in /etc/apt/sources.list +// e.g: +// deb|deb-src $origin $distribution $component1 $component2 +type aptSource struct { + DebType string + Origin string + Distribution string + Components []string +} + +// readAptSourcesFile reads a sources.list style file +// and return an aptSourcesFile +func readAptSourcesFile(path string) *aptSourcesFile { + fi, err := os.Open(path) + + if err != nil { + return nil + } + + ret := aptSourcesFile{Path: path} + + defer fi.Close() + sc := bufio.NewScanner(fi) + for sc.Scan() { + line := sc.Text() + + line = strings.TrimSpace(line) + + // Skip blanks.. + if len(line) < 1 { + continue + } + + // Skip comment lines + if line[0] == '#' { + continue + } + + if rs := newAptSource(line); rs != nil { + ret.Sources = append(ret.Sources, rs) + } + // Could warn here, but that's overkill. + } + return &ret +} + +// loadAptSources reads the apt source files +// defined by aptSourcesList and aptSourcesListD +// returning a list of aptSourcesFile containing +// all sources defined in the distro +func loadAptSources() []*aptSourcesFile { + var sources []*aptSourcesFile + + // Closure is only relevant to this function + addAptSources := func(path string) { + if r := readAptSourcesFile(path); r != nil { + sources = append(sources, r) + } + } + + if pathExists(aptSourcesList) { + addAptSources(aptSourcesList) + } + + // Glob the *.list files now + if pathExists(aptSourcesListD) { + tpath := fmt.Sprintf("%s/*.list", aptSourcesListD) + if files, err := filepath.Glob(tpath); err == nil { + for _, file := range files { + addAptSources(file) + } + } + } + + return sources +} + +// newAptSource constructs a new apt source from +// the given deb style line +func newAptSource(debLine string) *aptSource { + fields := strings.Fields(debLine) + var asource aptSource + + if len(fields) < 3 { + return nil + } + + dtype := fields[0] + if dtype != "deb" && dtype != "deb-src" { + return nil + } + + asource.DebType = dtype + asource.Origin = fields[1] + asource.Distribution = fields[2] + if len(fields) > 3 { + asource.Components = fields[3:] + } + + return &asource +} + +// isUbuntuDockerRepoEnabled iterates through the sources +// to find out if the docker repo is enabled or not. +func isUbuntuDockerRepoEnabled() bool { + sources := loadAptSources() + if sources == nil { + return false + } + for _, sourceFile := range sources { + for _, source := range sourceFile.Sources { + if strings.Contains(source.Origin, "apt.dockerproject.org") { + return true + } + } + } + return false +} + +// pathExists is a helper function which handles the +// error and simply return true or false if the given +// path exists +func pathExists(path string) bool { + if _, err := os.Stat(path); err != nil { + return false + } + return true +} + +type distro interface { + // InstallPackages should implement the installation + // of packages using distro specific methods for + // the given target list of items to install + InstallPackages(packages []string) bool + + // getID should return a string specifying + // the distribution ID (e.g: "clearlinux") + getID() string +} + +// getDistro will return a distro based on what +// is read from GetOsRelease +func getDistro() distro { + osRelease := GetOsRelease() + + if osRelease == nil { + return nil + } + + if strings.HasPrefix(osRelease.ID, "clear-linux") { + return &clearLinuxDistro{} + } else if strings.Contains(osRelease.ID, "ubuntu") { + // Store the Ubuntu codename, i.e. "xenial' + return &ubuntuDistro{CodeName: osRelease.GetValue("UBUNTU_CODENAME")} + } else if strings.Contains(osRelease.ID, "fedora") { + return &fedoraDistro{} + } + return nil +} + +// os-release clear-linux* +type clearLinuxDistro struct { +} + +func (d *clearLinuxDistro) getID() string { + return "clearlinux" +} + +// Correctly split and format the command, using sudo if appropriate, as a +// common mechanism for the various package install functions. +func sudoFormatCommand(command string, packages []string) bool { + toInstall := strings.Join(packages[0:], " ") + + var executable string + var args string + splits := strings.Split(command, " ") + + if syscall.Geteuid() == 0 { + executable = splits[0] + args = fmt.Sprintf(strings.Join(splits[1:], " "), toInstall) + } else { + executable = "sudo" + args = fmt.Sprintf(command, toInstall) + } + + c := exec.Command(executable, strings.Split(args, " ")...) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + if err := c.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Unable to run command: %s", err) + return false + } + return true +} + +func (d *clearLinuxDistro) InstallPackages(packages []string) bool { + return sudoFormatCommand("swupd bundle-add %s", packages) +} + +// os-release *ubuntu* +type ubuntuDistro struct { + CodeName string +} + +func (d *ubuntuDistro) getID() string { + return "ubuntu" +} + +func (d *ubuntuDistro) InstallPackages(packages []string) bool { + return sudoFormatCommand("apt-get --yes --force-yes install %s", packages) +} + +// Fedora +type fedoraDistro struct { +} + +func (d *fedoraDistro) getID() string { + return "fedora" +} + +// Use dnf to install on Fedora +func (d *fedoraDistro) InstallPackages(packages []string) bool { + return sudoFormatCommand("dnf install -y %s", packages) +} diff --git a/osprepare/distro_test.go b/osprepare/distro_test.go new file mode 100644 index 000000000..f54917f75 --- /dev/null +++ b/osprepare/distro_test.go @@ -0,0 +1,34 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "testing" +) + +func TestGetDistro(t *testing.T) { + if pathExists("/usr/share/clear/bundles") == false { + t.Skip("Unsupported test distro") + } + d := getDistro() + if d == nil { + t.Fatal("Cannot get known distro object") + } + if d.getID() == "" { + t.Fatal("Invalid ID for distro") + } +} diff --git a/osprepare/main.go b/osprepare/main.go new file mode 100644 index 000000000..c619b2c80 --- /dev/null +++ b/osprepare/main.go @@ -0,0 +1,99 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "fmt" + "os" + "strings" +) + +// Minimal versions supported by ciao +const ( + MinDockerVersion = "1.11.0" + MinQemuVersion = "2.5.0" +) + +// PackageRequirement contains the BinaryName expected to +// exist on the filesystem once PackageName is installed +// (e.g: { '/usr/bin/qemu-system-x86_64', 'qemu'}) +type PackageRequirement struct { + BinaryName string + PackageName string +} + +// PackageRequirements type allows to create complex +// mapping to group a set of PackageRequirement to a single +// key. +// (e.g: +// +// "ubuntu": { +// {"/usr/bin/docker", "docker"}, +// }, +// "clearlinux": { +// {"/usr/bin/docker", "containers-basic"}, +// }, +// ) +type PackageRequirements map[string][]*PackageRequirement + +// CollectPackages returns a list of non-installed packages from +// the PackageRequirements received +func collectPackages(dist distro, reqs *PackageRequirements) []string { + // For now just support keys like "ubuntu" vs "ubuntu:16.04" + var pkgsMissing []string + if reqs == nil { + return nil + } + + id := dist.getID() + if pkgs, success := (*reqs)[id]; success { + for _, pkg := range pkgs { + // Have the path existing, skip. + if pathExists(pkg.BinaryName) { + continue + } + // Mark the package for installation + pkgsMissing = append(pkgsMissing, pkg.PackageName) + } + return pkgsMissing + } + return nil +} + +// PrepareCIAO installs all the dependencies defined in +// PackageRequirements in order to run the ciao component +func PrepareCIAO(reqs *PackageRequirements) bool { + distro := getDistro() + + if distro == nil { + fmt.Fprintf(os.Stderr, "Running on an unsupported distro\n") + if rel := GetOsRelease(); rel != nil { + fmt.Fprintf(os.Stderr, "Unsupported distro: %s %s\n", rel.Name, rel.Version) + } else { + fmt.Fprintln(os.Stderr, "No os-release found on this host") + } + return false + } + fmt.Println(distro.getID()) + if reqPkgs := collectPackages(distro, reqs); reqPkgs != nil { + if distro.InstallPackages(reqPkgs) == false { + fmt.Fprintf(os.Stderr, "Failed to install: %s\n", strings.Join(reqPkgs, ", ")) + return false + } + } + return true +} diff --git a/osprepare/os_release.go b/osprepare/os_release.go new file mode 100644 index 000000000..b8d7d9482 --- /dev/null +++ b/osprepare/os_release.go @@ -0,0 +1,98 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "bufio" + "os" + "strings" +) + +type OsRelease struct { + Name string + ID string + PrettyName string + Version string + VersionID string + mapping map[string]string +} + +// Parse the given path and attempt to return a valid +// OsRelease for it +func ParseReleaseFile(path string) *OsRelease { + fi, err := os.Open(path) + var os_rel OsRelease + os_rel.mapping = make(map[string]string) + + if err != nil { + return nil + } + defer fi.Close() + sc := bufio.NewScanner(fi) + for sc.Scan() { + line := sc.Text() + + spl := strings.Split(line, "=") + if len(spl) < 2 { + continue + } + key := strings.ToLower(strings.TrimSpace(spl[0])) + value := strings.TrimSpace(strings.Join(spl[1:], "=")) + + value = strings.Replace(value, "\"", "", -1) + value = strings.Replace(value, "'", "", -1) + + if key == "name" { + os_rel.Name = value + } else if key == "id" { + os_rel.ID = value + } else if key == "pretty_name" { + os_rel.PrettyName = value + } else if key == "version" { + os_rel.Version = value + } else if key == "version_id" { + os_rel.VersionID = value + } + + // Store it for use by Distro + os_rel.mapping[key] = value + } + return &os_rel +} + +// Try all known paths to get the right OsRelease instance +func GetOsRelease() *OsRelease { + paths := []string{ + "/etc/os-release", + "/usr/lib/os-release", + "/usr/lib64/os-release", + } + + for _, item := range paths { + if os_rel := ParseReleaseFile(item); os_rel != nil { + return os_rel + } + } + return nil +} + +func (o *OsRelease) GetValue(key string) string { + if val, succ := o.mapping[strings.ToLower(key)]; succ { + return val + } + return "" +} diff --git a/osprepare/os_release_test.go b/osprepare/os_release_test.go new file mode 100644 index 000000000..992c071eb --- /dev/null +++ b/osprepare/os_release_test.go @@ -0,0 +1,50 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "strings" + "testing" +) + +const ( + NON_EXISTENT_FILE = "/nonexistentpath/this/file/doesnt/exists" +) + +func TestGetOsRelease(t *testing.T) { + d := getDistro() + if d == nil { + t.Skip("Unknown distro, cannot test") + } + os_rel := GetOsRelease() + if os_rel == nil { + t.Fatal("Could not get os-release file for known distro") + } + if d.getID() == "clearlinux" && !strings.Contains(os_rel.ID, "clear") { + t.Fatal("Invalid os-release for clearlinux") + } else if d.getID() == "ubuntu" && !strings.Contains(os_rel.ID, "ubuntu") { + t.Fatal("Invalid os-release for Ubuntu") + } else if d.getID() == "fedora" && !strings.Contains(os_rel.ID, "fedora") { + t.Fatal("Invalid os-release for Fedora") + } +} + +func TestParseReleaseFileNonExistent(t *testing.T) { + if res := ParseReleaseFile(NON_EXISTENT_FILE); res != nil { + t.Fatal("Expected nil, got %v\n", res) + } +} diff --git a/osprepare/versions.go b/osprepare/versions.go new file mode 100644 index 000000000..314a63c79 --- /dev/null +++ b/osprepare/versions.go @@ -0,0 +1,108 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" +) + +func get_command_output(command string) string { + splits := strings.Split(command, " ") + c := exec.Command(splits[0], splits[1:]...) + c.Env = os.Environ() + // Force C locale + c.Env = append(c.Env, "LC_ALL=C") + c.Env = append(c.Env, "LANG=C") + c.Stderr = os.Stderr + + if out, err := c.Output(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to run %s: %s\n", splits[0], err) + return "" + } else { + return string(out) + } +} + +func GetDockerVersion() string { + ret := get_command_output("docker --version") + var version string + if n, _ := fmt.Sscanf(ret, "Docker version %s, build", &version); n != 1 { + return "" + } else { + if strings.HasSuffix(version, ",") { + return string(version[0 : len(version)-1]) + } + return version + } +} + +func GetQemuVersion() string { + ret := get_command_output("qemu-system-x86_64 --version") + var version string + if n, _ := fmt.Sscanf(ret, "QEMU emulator version %s, Copyright (c)", &version); n != 1 { + return "" + } else { + if strings.HasSuffix(version, ",") { + return string(version[0 : len(version)-1]) + } + return version + } +} + +// Determine if the given current version is less than the test version +// Note: Can only compare equal version schemas (i.e. same level of dots) +func VersionLessThan(current_version string, test_version string) bool { + cur_splits := strings.Split(current_version, ".") + test_splits := strings.Split(test_version, ".") + + max_range := len(cur_splits) + if l2 := len(test_splits); l2 < max_range { + max_range = l2 + } + + cur_isplits := make([]int, max_range) + cur_tsplits := make([]int, max_range) + + for i := 0; i < max_range; i++ { + cur_isplits[i], _ = strconv.Atoi(cur_splits[i]) + cur_tsplits[i], _ = strconv.Atoi(test_splits[i]) + } + + for i := 0; i < max_range; i++ { + if i == 0 { + if cur_isplits[i] < cur_tsplits[i] { + return true + } + } else { + match := true + for j := 0; j < i; j++ { + if cur_isplits[j] != cur_tsplits[j] { + match = false + break + } + } + if match && cur_isplits[i] < cur_tsplits[i] { + return true + } + } + } + return false +} diff --git a/osprepare/versions_test.go b/osprepare/versions_test.go new file mode 100644 index 000000000..ae84f56c0 --- /dev/null +++ b/osprepare/versions_test.go @@ -0,0 +1,66 @@ +// +// Copyright © 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package osprepare + +import ( + "testing" +) + +func TestGetDocker(t *testing.T) { + if pathExists("/usr/bin/docker") == false { + t.Skip("Docker not installed, cannot validate version get") + } + if vers := GetDockerVersion(); vers == "" { + t.Fatal("Cannot determine docker version") + } +} + +func TestGetQemu(t *testing.T) { + if pathExists("/usr/bin/qemu-system-x86_64") == false { + t.Skip("Qemu not installed, cannot validate version get") + } + if vers := GetQemuVersion(); vers == "" { + t.Fatal("Cannot determine qemu version") + } +} + +// TestVersionLessThanEqualVersion tests than VersionLessThan returns +// false when given same version to tests. e.g: VersionLessThan("1.11.0", "1.11.0") +// this tests is expected to pass +func TestVersionLessThanEqualVersion(t *testing.T) { + if res := VersionLessThan(MinQemuVersion, MinQemuVersion); res != false { + t.Fatal("expected false, got %v\n", res) + } +} + +// TestVersionLessThanGreaterVersion tests than VersionLessThan returns +// false when given greater version. e.g: VersionLessThan("1.11.0", "0.0.1") +// this tests is expected to pass +func TestVersionLessThanGreaterVersion(t *testing.T) { + if res := VersionLessThan(MinQemuVersion, "0.0.1"); res != false { + t.Fatal("expected false, got %v\n", res) + } +} + +// TestVersionLessThanLowerVersion tests than VersionLessThan returns +// true when given lower version. e.g: VersionLessThan("0.0.1", "99.9.9") +// this tests is expected to pass +func TestVersionLessThanLowerVersion(t *testing.T) { + if res := VersionLessThan("0.0.1", MinQemuVersion); res != true { + t.Fatal("expected true, got %v\n", res) + } +}