Skip to content
This repository was archived by the owner on Mar 27, 2024. It is now read-only.

Try to use local rpm binary to query rpmdb #244

Merged
merged 2 commits into from
Jun 26, 2018
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions differs/rpm_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ limitations under the License.
package differs

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
Expand All @@ -40,6 +43,11 @@ import (
"github.com/sirupsen/logrus"
)

//RPM command to extract packages from the rpm database
var rpmCmd = []string{
"rpm", "--nodigest", "--nosignature",
"-qa", "--qf", "%{NAME}\t%{VERSION}\t%{SIZE}\n",
}
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

// daemonMutex is required to protect against other go-routines, as
Expand Down Expand Up @@ -87,7 +95,65 @@ func (a RPMAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageIn
}
}

return rpmDataFromContainer(image)
packages, err := rpmDataFromImageFS(image)
if err != nil {
logrus.Warn("Trying to run the RPM binary of the image in a container")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's log this at Info level

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Running RPM binary from image in a container"

return rpmDataFromContainer(image)
}
return packages, err
}

// rpmDataFromImageFS runs a local rpm binary, if any, to query the image
// rpmdb and returns a map of installed packages.
func rpmDataFromImageFS(image pkgutil.Image) (map[string]util.PackageInfo, error) {
packages := make(map[string]util.PackageInfo)
// Check there is an executable rpm tool in host
if err := exec.Command("rpm", "--version").Run(); err != nil {
logrus.Warn("No RPM binary in host")
return packages, err
}
dbPath, err := rpmDBPath(image.FSPath)
if err != nil {
logrus.Warnf("Couldn't find RPM database: %s", err.Error())
return packages, err
}
cmdArgs := append([]string{"--root", image.FSPath, "--dbpath", dbPath}, rpmCmd[1:]...)
out, err := exec.Command(rpmCmd[0], cmdArgs...).Output()
if err != nil {
logrus.Warnf("RPM call failed: %s", err.Error())
return packages, err
}
output := strings.Split(string(out), "\n")
return parsePackageData(output)
}

// rpmDBPath tries to get the RPM database path from the /usr/lib/rpm/macros
// file in the image rootfs.
func rpmDBPath(rootFSPath string) (string, error) {
rpmMacros, err := os.Open(filepath.Join(rootFSPath, "usr/lib/rpm/macros"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this path a constant

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible that the RPM database could have been put in a different location by the creator of the image? are there other common locations that we might want to check here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are looking for the value of %_dbpath macro which sets the path of the database.

is it possible that the RPM database could have been put in a different location by the creator of the image?

Yes, it could be the case that the user sets its own %_dbpath in a custom user defined macro. As far as I saw in fedora, opensuse and centos, the %_dbpath defined in /usr/lib/rpm/macros is not overwritten in any of them. According to the man pages RPM looks for macros in:

The default FILELIST is /usr/lib/rpm/macros:/usr/lib/rpm/macros.d/macros.*:/usr/lib/rpm/platform/%{_target}/macros:/usr/lib/rpm/fileattrs/*.attr:/usr/lib/rpm/redhat/macros:/etc/rpm/macros.*:/etc/rpm/macros:/etc/rpm/%{_target}/macros:~/.rpmmacros

The macros from the image could be loaded by using the --macros rpm flag and passing the appropriate paths list. However I encountered that this flag expects a colon separated list of paths and I could not manage to escape colons that they are part of the path we want to provide (i.e providing --macros opensuse@sha256:8e70aa9ec2/usr/lib/rpm/macros is understood as two separate paths 🤦‍♂️ and I could not manage to escape the colons).

That's the reason to manually parse the /usr/lib/rpm/macros file to figure out the rpmdb path. Note that then is verified the path exists and if the command fails it falls back to running the container. I tested this approach of parsing the main macros file in opensuse, centos and fedora with success.

if err != nil {
return "", err
}
defer rpmMacros.Close()

scanner := bufio.NewScanner(rpmMacros)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "%_dbpath") {
fields := strings.Fields(line)
if len(fields) < 2 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a comment with an example of what you're trying to match for here? maybe a regex would be more clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a comment with the example, I think this is the clearest.

break
}
out, err := exec.Command("rpm", "-E", fields[1]).Output()
if err != nil {
return "", err
}
dbPath := strings.TrimRight(string(out), "\r\n")
_, err = os.Stat(filepath.Join(rootFSPath, dbPath))
return dbPath, err
}
}
return "", errors.New("Failed parsing macros file")
}

// rpmDataFromContainer runs image in a container, queries the data of
Expand All @@ -114,7 +180,7 @@ func rpmDataFromContainer(image pkgutil.Image) (map[string]util.PackageInfo, err
defer logrus.Infof("Removing image %s", imageName)

contConf := godocker.Config{
Entrypoint: []string{"rpm", "--nodigest", "--nosignature", "-qa", "--qf", "%{NAME}\t%{VERSION}\t%{SIZE}\n"},
Entrypoint: rpmCmd,
Image: imageName,
}

Expand Down