github.com/coreos/mantle@v0.13.0/kola/tests/ostree/basic.go (about)

     1  // Copyright 2018 Red Hat, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ostree
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/coreos/mantle/kola/cluster"
    24  	"github.com/coreos/mantle/kola/register"
    25  	"github.com/coreos/mantle/kola/tests/util"
    26  	"github.com/coreos/mantle/platform"
    27  )
    28  
    29  // the "basic" test is only supported on 'rhcos' for now because of how
    30  // the refs are defined. if 'fcos' goes in the same direction, we can
    31  // expand support there.
    32  func init() {
    33  	register.Register(&register.Test{
    34  		Run:         ostreeBasicTest,
    35  		ClusterSize: 1,
    36  		Name:        "ostree.basic",
    37  		Distros:     []string{"rhcos"},
    38  		FailFast:    true,
    39  	})
    40  
    41  	register.Register(&register.Test{
    42  		Run:         ostreeRemoteTest,
    43  		ClusterSize: 1,
    44  		Name:        "ostree.remote",
    45  		Distros:     []string{"fcos", "rhcos"},
    46  		Flags:       []register.Flag{register.RequiresInternetAccess}, // need network to contact remote
    47  		FailFast:    true,
    48  	})
    49  }
    50  
    51  type ostreeAdminStatus struct {
    52  	Checksum string
    53  	Origin   string
    54  	Version  string
    55  }
    56  
    57  // getOstreeRemotes returns the current number of ostree remotes on a machine
    58  func getOstreeRemotes(c cluster.TestCluster, m platform.Machine) (int, []string) {
    59  	remoteListOut := string(c.MustSSH(m, "ostree remote list"))
    60  	numRemotes := 0
    61  	// If we get anything other than an empty string calculate the results
    62  	// NOTE: This is needed as splitting "" ends up providing a count of 1
    63  	//       when the count should be 0
    64  	remoteListRaw := strings.Split(remoteListOut, "\n")
    65  	if remoteListOut != "" {
    66  		numRemotes = len(remoteListRaw)
    67  	}
    68  	return numRemotes, remoteListRaw
    69  }
    70  
    71  // getOstreeAdminStatus stuffs the important output of `ostree admin status`
    72  // into an `ostreeAdminStatus` struct
    73  func getOstreeAdminStatus(c cluster.TestCluster, m platform.Machine) (ostreeAdminStatus, error) {
    74  	oaStatus := ostreeAdminStatus{}
    75  
    76  	oasOutput, err := c.SSH(m, "ostree admin status")
    77  	if err != nil {
    78  		return oaStatus, fmt.Errorf(`Could not get "ostree admin status": %v`, err)
    79  	}
    80  
    81  	oasSplit := strings.Split(string(oasOutput), "\n")
    82  	if len(oasSplit) < 3 {
    83  		return oaStatus, fmt.Errorf(`Unexpected output from "ostree admin status": %v`, string(oasOutput))
    84  	}
    85  
    86  	// we use a bunch of regexps to find the content in each line of output
    87  	// from "ostree admin status".  the `match` for each line sticks the
    88  	// captured group as the last element of the array; that is used as the
    89  	// value for each field of the struct
    90  	reCsum, _ := regexp.Compile(`^\* [\w\-]+ ([0-9a-f]+)\.\d`)
    91  	csumMatch := reCsum.FindStringSubmatch(oasSplit[0])
    92  	if csumMatch == nil {
    93  		return oaStatus, fmt.Errorf(`Could not parse first line from "ostree admin status": %q`, oasSplit[0])
    94  	}
    95  	oaStatus.Checksum = csumMatch[len(csumMatch)-1]
    96  
    97  	reVersion, _ := regexp.Compile(`^Version: (.*)`)
    98  	versionMatch := reVersion.FindStringSubmatch(strings.TrimSpace(oasSplit[1]))
    99  	if versionMatch == nil {
   100  		return oaStatus, fmt.Errorf(`Could not parse second line from "ostree admin status": %q`, oasSplit[1])
   101  	}
   102  	oaStatus.Version = versionMatch[len(versionMatch)-1]
   103  
   104  	reOrigin, _ := regexp.Compile(`^origin refspec: (.*)`)
   105  	originMatch := reOrigin.FindStringSubmatch(strings.TrimSpace(oasSplit[2]))
   106  	if originMatch == nil {
   107  		return oaStatus, fmt.Errorf(`Could not parse third line from "ostree admin status": %q`, oasSplit[2])
   108  	}
   109  	oaStatus.Origin = originMatch[len(originMatch)-1]
   110  
   111  	return oaStatus, nil
   112  }
   113  
   114  // ostreeBasicTest performs sanity checks on the output from `ostree admin status`,
   115  // `ostree rev-parse`, and `ostree show` by comparing to the output from `rpm-ostree status`
   116  func ostreeBasicTest(c cluster.TestCluster) {
   117  	m := c.Machines()[0]
   118  
   119  	ros, err := util.GetRpmOstreeStatusJSON(c, m)
   120  	if err != nil {
   121  		c.Fatal(err)
   122  	}
   123  
   124  	oas, err := getOstreeAdminStatus(c, m)
   125  	if err != nil {
   126  		c.Fatal(err)
   127  	}
   128  
   129  	if len(ros.Deployments) < 1 {
   130  		c.Fatalf(`Did not find any deployments?!`)
   131  	}
   132  
   133  	// verify the output from `ostree admin status`
   134  	c.Run("admin status", func(c cluster.TestCluster) {
   135  		if oas.Checksum != ros.Deployments[0].Checksum {
   136  			c.Fatalf(`Checksums do not match; expected %q, got %q`, ros.Deployments[0].Checksum, oas.Checksum)
   137  		}
   138  		if oas.Version != ros.Deployments[0].Version {
   139  			c.Fatalf(`Versions do not match; expected %q, got %q`, ros.Deployments[0].Version, oas.Version)
   140  		}
   141  		if oas.Origin != ros.Deployments[0].Origin {
   142  			c.Fatalf(`Origins do not match; expected %q, got %q`, ros.Deployments[0].Origin, oas.Origin)
   143  		}
   144  	})
   145  
   146  	// verify the output from `ostree rev-parse`
   147  	// this is kind of moot since the origin for RHCOS is just
   148  	// the checksum now
   149  	c.Run("rev-parse", func(c cluster.TestCluster) {
   150  		// check the output of `ostree rev-parse`
   151  		oRevParseOut := c.MustSSH(m, ("ostree rev-parse " + oas.Origin))
   152  
   153  		if string(oRevParseOut) != oas.Checksum {
   154  			c.Fatalf(`Checksum from "ostree rev-parse" does not match; expected %q, got %q`, oas.Checksum, string(oRevParseOut))
   155  		}
   156  	})
   157  
   158  	// verify the output of 'ostree show'
   159  	c.Run("show", func(c cluster.TestCluster) {
   160  		oShowOut := c.MustSSH(m, ("ostree show " + oas.Checksum))
   161  		oShowOutSplit := strings.Split(string(oShowOut), "\n")
   162  		// we need at least the first 4 lines (commit, ContentChecksum, Date, Version)
   163  		// to proceed safely
   164  		if len(oShowOutSplit) < 4 {
   165  			c.Fatalf(`Unexpected output from "ostree show": %q`, string(oShowOut))
   166  		}
   167  
   168  		// convert the 'timestamp' from `rpm-ostree status` into a date that
   169  		// we can compare to the date in `ostree admin status`
   170  		// also, wtf is up with formatting date/time in golang?!
   171  		timeFormat := "2006-01-02 15:04:05 +0000"
   172  		tsUnix := time.Unix(ros.Deployments[0].Timestamp, 0).UTC()
   173  		tsFormatted := tsUnix.Format(timeFormat)
   174  		oShowDate := strings.TrimPrefix(oShowOutSplit[2], "Date:  ")
   175  
   176  		if oShowDate != tsFormatted {
   177  			c.Fatalf(`Dates do not match; expected %q, got %q`, tsFormatted, oShowDate)
   178  		}
   179  
   180  		oVersionSplit := strings.Fields(oShowOutSplit[3])
   181  		if len(oVersionSplit) < 2 {
   182  			c.Fatalf(`Unexpected content in "Version" field of "ostree show" output: %q`, oShowOutSplit[3])
   183  		}
   184  		if oVersionSplit[1] != ros.Deployments[0].Version {
   185  			c.Fatalf(`Versions do not match; expected %q, got %q`, ros.Deployments[0].Version, oVersionSplit[1])
   186  		}
   187  	})
   188  }
   189  
   190  // ostreeRemoteTest verifies the `ostree remote` functionality;
   191  // specifically:  `add`, `delete`, `list`, `refs`, `show-url`, `summary`
   192  func ostreeRemoteTest(c cluster.TestCluster) {
   193  	m := c.Machines()[0]
   194  
   195  	// Get the initial amount of remotes configured
   196  	initialRemotesNum, _ := getOstreeRemotes(c, m)
   197  
   198  	// TODO: if this remote ever changes, update the `refMatch` regexp
   199  	// in the `ostree remote summary` test below
   200  	remoteName := "custom"
   201  	remoteUrl := "https://dl.fedoraproject.org/atomic/repo/"
   202  
   203  	// verify `ostree remote add` is successful
   204  	c.Run("add", func(c cluster.TestCluster) {
   205  		osRemoteAddCmd := "sudo ostree remote add --no-gpg-verify " + remoteName + " " + remoteUrl
   206  		c.MustSSH(m, osRemoteAddCmd)
   207  	})
   208  
   209  	// verify `ostree remote list`
   210  	c.Run("list", func(c cluster.TestCluster) {
   211  		osRemoteListOut := c.MustSSH(m, "ostree remote list -u")
   212  
   213  		osRemoteListSplit := strings.Split(string(osRemoteListOut), "\n")
   214  		// should have original remote + newly added remote
   215  		if len(osRemoteListSplit) != initialRemotesNum+1 {
   216  			c.Fatalf(`Did not find expected amount of ostree remotes: %q. Expected %d`, string(osRemoteListOut), osRemoteListSplit)
   217  		}
   218  
   219  		var remoteFound bool = false
   220  		for _, v := range osRemoteListSplit {
   221  			lSplit := strings.Fields(v)
   222  			if len(lSplit) != 2 {
   223  				c.Fatalf(`Unexpected format of ostree remote entry: %q`, v)
   224  			}
   225  			if lSplit[0] == remoteName && lSplit[1] == remoteUrl {
   226  				remoteFound = true
   227  			}
   228  		}
   229  		if !remoteFound {
   230  			c.Fatalf(`Added remote was not found: %v`, string(osRemoteListOut))
   231  		}
   232  	})
   233  
   234  	// verify `ostree remote show-url`
   235  	c.Run("show-url", func(c cluster.TestCluster) {
   236  		osRemoteShowUrlOut := c.MustSSH(m, ("ostree remote show-url " + remoteName))
   237  		if string(osRemoteShowUrlOut) != remoteUrl {
   238  			c.Fatalf(`URLs don't match; expected %q, got %q`, remoteName, string(osRemoteShowUrlOut))
   239  		}
   240  	})
   241  
   242  	// verify `ostree remote refs`
   243  	c.Run("refs", func(c cluster.TestCluster) {
   244  		osRemoteRefsOut := c.MustSSH(m, ("ostree remote refs " + remoteName))
   245  		if len(strings.Split(string(osRemoteRefsOut), "\n")) < 1 {
   246  			c.Fatalf(`Did not receive expected amount of refs from remote: %v`, string(osRemoteRefsOut))
   247  		}
   248  	})
   249  
   250  	// verify `ostree remote summary`
   251  	c.Run("summary", func(c cluster.TestCluster) {
   252  		remoteRefsOut := c.MustSSH(m, ("ostree remote refs " + remoteName))
   253  		remoteRefsOutSplit := strings.Split(string(remoteRefsOut), "\n")
   254  		remoteRefsCount := len(remoteRefsOutSplit)
   255  		if remoteRefsCount < 1 {
   256  			c.Fatalf(`Did not find any refs on ostree remote: %q`, string(remoteRefsOut))
   257  		}
   258  
   259  		osRemoteSummaryOut := c.MustSSH(m, ("ostree remote summary " + remoteName))
   260  		if len(strings.Split(string(osRemoteSummaryOut), "\n")) < 1 {
   261  			c.Fatalf(`Did not receive expected summary content from remote: %v`, string(osRemoteSummaryOut))
   262  		}
   263  
   264  		// the remote summary *should* include the same amount of
   265  		// refs as found in `ostree remote refs`
   266  		var refCount int = 0
   267  
   268  		// this matches the line from the output that includes the name of the ref.
   269  		// the original remote used in this test is a Fedora remote, so we are checking
   270  		// for refs particular to that remote.
   271  		// TODO: if the remote ever changes, we'll have to change this regexp too.
   272  		refMatch := regexp.MustCompile(`^\* fedora.*`)
   273  		remoteSummaryOutSplit := strings.Split(string(osRemoteSummaryOut), "\n")
   274  		// the remote should have at least one ref in the summary
   275  		if len(remoteSummaryOutSplit) < 4 {
   276  			c.Fatalf(`Did not find any refs in the remote summary: %q`, string(osRemoteSummaryOut))
   277  		}
   278  		for _, v := range remoteSummaryOutSplit {
   279  			if refMatch.MatchString(v) {
   280  				refCount += 1
   281  			}
   282  		}
   283  
   284  		if refCount == 0 {
   285  			c.Fatalf(`Did not find any refs in the remote summary that matched!`)
   286  		}
   287  
   288  		if refCount != remoteRefsCount {
   289  			c.Fatalf(`Did not find correct number of refs in remote summary; expected %q, got %q`, remoteRefsCount, refCount)
   290  		}
   291  	})
   292  
   293  	// verify `ostree remote delete`
   294  	c.Run("delete", func(c cluster.TestCluster) {
   295  		preRemotesOut := c.MustSSH(m, "ostree remote list")
   296  		preNumRemotes := len(strings.Split(string(preRemotesOut), "\n"))
   297  
   298  		if preNumRemotes < 1 {
   299  			c.Fatalf(`No remotes configured on host: %q`, string(preRemotesOut))
   300  		}
   301  
   302  		c.MustSSH(m, ("sudo ostree remote delete " + remoteName))
   303  
   304  		delNumRemotes, delRemoteListOut := getOstreeRemotes(c, m)
   305  		if delNumRemotes >= preNumRemotes {
   306  			c.Fatalf(`Number of remotes did not decrease after "ostree delete": %s`, delRemoteListOut)
   307  		}
   308  	})
   309  }