gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/custom_device_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2022 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package builtin_test
    21  
    22  import (
    23  	"fmt"
    24  	"strings"
    25  
    26  	. "gopkg.in/check.v1"
    27  
    28  	"gitee.com/mysnapcore/mysnapd/dirs"
    29  	"gitee.com/mysnapcore/mysnapd/interfaces"
    30  	"gitee.com/mysnapcore/mysnapd/interfaces/apparmor"
    31  	"gitee.com/mysnapcore/mysnapd/interfaces/builtin"
    32  	"gitee.com/mysnapcore/mysnapd/interfaces/udev"
    33  	"gitee.com/mysnapcore/mysnapd/snap"
    34  	"gitee.com/mysnapcore/mysnapd/testutil"
    35  )
    36  
    37  type CustomDeviceInterfaceSuite struct {
    38  	testutil.BaseTest
    39  
    40  	iface    interfaces.Interface
    41  	slotInfo *snap.SlotInfo
    42  	slot     *interfaces.ConnectedSlot
    43  	plugInfo *snap.PlugInfo
    44  	plug     *interfaces.ConnectedPlug
    45  }
    46  
    47  var _ = Suite(&CustomDeviceInterfaceSuite{
    48  	iface: builtin.MustInterface("custom-device"),
    49  })
    50  
    51  const customDeviceConsumerYaml = `name: consumer
    52  version: 0
    53  plugs:
    54   hwdev:
    55    interface: custom-device
    56    custom-device: foo
    57  apps:
    58   app:
    59    plugs: [hwdev]
    60  `
    61  
    62  const customDeviceProviderYaml = `name: provider
    63  version: 0
    64  slots:
    65   hwdev:
    66    interface: custom-device
    67    custom-device: foo
    68    devices:
    69      - /dev/input/event[0-9]
    70      - /dev/input/mice
    71    read-devices:
    72      - /dev/js*
    73    files:
    74      write: [ /bar ]
    75      read:
    76        - /dev/input/by-id/*
    77    udev-tagging:
    78      - kernel: input/mice
    79        subsystem: input
    80        attributes:
    81          attr1: one
    82          attr2: two
    83        environment:
    84          env1: first
    85          env2: second|other
    86  apps:
    87   app:
    88    slots: [hwdev]
    89  `
    90  
    91  func (s *CustomDeviceInterfaceSuite) SetUpTest(c *C) {
    92  	s.BaseTest.SetUpTest(c)
    93  
    94  	s.plug, s.plugInfo = MockConnectedPlug(c, customDeviceConsumerYaml, nil, "hwdev")
    95  	s.slot, s.slotInfo = MockConnectedSlot(c, customDeviceProviderYaml, nil, "hwdev")
    96  }
    97  
    98  func (s *CustomDeviceInterfaceSuite) TestName(c *C) {
    99  	c.Assert(s.iface.Name(), Equals, "custom-device")
   100  }
   101  
   102  func (s *CustomDeviceInterfaceSuite) TestSanitizePlug(c *C) {
   103  	c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
   104  	c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil)
   105  }
   106  
   107  func (s *CustomDeviceInterfaceSuite) TestSanitizePlugUnhappy(c *C) {
   108  	var customDeviceYaml = `name: consumer
   109  version: 0
   110  plugs:
   111   hwdev:
   112    interface: custom-device
   113    %s
   114  apps:
   115   app:
   116    plugs: [hwdev]
   117  `
   118  	data := []struct {
   119  		plugYaml      string
   120  		expectedError string
   121  	}{
   122  		{
   123  			"custom-device: [one two]",
   124  			`custom-device "custom-device" attribute must be a string, not \[one two\]`,
   125  		},
   126  	}
   127  
   128  	for _, testData := range data {
   129  		snapYaml := fmt.Sprintf(customDeviceYaml, testData.plugYaml)
   130  		_, plug := MockConnectedPlug(c, snapYaml, nil, "hwdev")
   131  		err := interfaces.BeforePreparePlug(s.iface, plug)
   132  		c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.plugYaml))
   133  	}
   134  }
   135  
   136  func (s *CustomDeviceInterfaceSuite) TestPlugNameAttribute(c *C) {
   137  	var plugYamlTemplate = `name: consumer
   138  version: 0
   139  plugs:
   140   hwdev:
   141    interface: custom-device
   142    %s
   143  apps:
   144   app:
   145    plugs: [hwdev]
   146  `
   147  
   148  	data := []struct {
   149  		plugYaml     string
   150  		expectedName string
   151  	}{
   152  		{
   153  			"",      // missing "custom-device" attribute
   154  			"hwdev", // use the name of the plug
   155  		},
   156  		{
   157  			"custom-device: shmemFoo",
   158  			"shmemFoo",
   159  		},
   160  	}
   161  
   162  	for _, testData := range data {
   163  		snapYaml := fmt.Sprintf(plugYamlTemplate, testData.plugYaml)
   164  		_, plug := MockConnectedPlug(c, snapYaml, nil, "hwdev")
   165  		err := interfaces.BeforePreparePlug(s.iface, plug)
   166  		c.Assert(err, IsNil)
   167  		c.Check(plug.Attrs["custom-device"], Equals, testData.expectedName,
   168  			Commentf(`yaml: %q`, testData.plugYaml))
   169  	}
   170  }
   171  
   172  func (s *CustomDeviceInterfaceSuite) TestSanitizeSlot(c *C) {
   173  	c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
   174  }
   175  
   176  func (s *CustomDeviceInterfaceSuite) TestSanitizeSlotUnhappy(c *C) {
   177  	var customDeviceYaml = `name: provider
   178  version: 0
   179  slots:
   180   hwdev:
   181    interface: custom-device
   182    %s
   183  apps:
   184   app:
   185    slots: [hwdev]
   186  `
   187  	data := []struct {
   188  		slotYaml      string
   189  		expectedError string
   190  	}{
   191  		{
   192  			"custom-device: [one two]",
   193  			`custom-device "custom-device" attribute must be a string, not \[one two\]`,
   194  		},
   195  		{
   196  			"devices: 12",
   197  			`snap "provider" has interface "custom-device" with invalid value type int64 for "devices" attribute.*`,
   198  		},
   199  		{
   200  			"read-devices: [/dev/zero, 2]",
   201  			`snap "provider" has interface "custom-device" with invalid value type \[\]interface {} for "read-devices" attribute.*`,
   202  		},
   203  		{
   204  			"devices: [/dev/foo**]",
   205  			`custom-device "devices" path contains invalid glob pattern "\*\*"`,
   206  		},
   207  		{
   208  			"devices: [/dev/@foo]",
   209  			`custom-device "devices" path must start with / and cannot contain special characters.*`,
   210  		},
   211  		{
   212  			"devices: [/dev/foo|bar]",
   213  			`custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
   214  		},
   215  		{
   216  			`devices: [/dev/foo"bar]`,
   217  			`custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
   218  		},
   219  		{
   220  			`devices: ["/dev/{foo}bar"]`,
   221  			`custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
   222  		},
   223  		{
   224  			"read-devices: [/dev/foo\\bar]",
   225  			`custom-device "read-devices" path must start with /dev/ and cannot contain special characters.*`,
   226  		},
   227  		{
   228  			"devices: [/run/foo]",
   229  			`custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
   230  		},
   231  		{
   232  			"devices: [/dev/../etc/passwd]",
   233  			`custom-device "devices" path is not clean.*`,
   234  		},
   235  		{
   236  			`read-devices: ["/dev/unmatched[bracket"]`,
   237  			`custom-device "read-devices" path cannot be used: missing closing bracket ']'.*`,
   238  		},
   239  		{
   240  			"devices: [/dev/foo]\n  read-devices: [/dev/foo]",
   241  			`cannot specify path "/dev/foo" both in "devices" and "read-devices" attributes`,
   242  		},
   243  		{
   244  			`files: {read: [23]}`,
   245  			`snap "provider" has interface "custom-device" with invalid value type map\[string\]interface {} for "files" attribute.*`,
   246  		},
   247  		{
   248  			`files: {write: [23]}`,
   249  			`snap "provider" has interface "custom-device" with invalid value type map\[string\]interface {} for "files" attribute.*`,
   250  		},
   251  		{
   252  			`files: {foo: [ /etc/foo ]}`,
   253  			`cannot specify \"foo\" in \"files\" section, only \"read\" and \"write\" allowed`,
   254  		},
   255  		{
   256  			`files: {read: [etc]}`,
   257  			`custom-device "read" path must start with / and cannot contain special characters.*`,
   258  		},
   259  		{
   260  			`files: {write: [one, 2]}`,
   261  			`snap "provider" has interface "custom-device" with invalid value type map\[string\]interface {} for "files" attribute.*`,
   262  		},
   263  		{
   264  			`files: {read: [/etc/foo], write: [one, 2]}`,
   265  			`snap "provider" has interface "custom-device" with invalid value type map\[string\]interface {} for "files" attribute.*`,
   266  		},
   267  		{
   268  			`files: {read: [222], write: [/etc/one]}`,
   269  			`snap "provider" has interface "custom-device" with invalid value type map\[string\]interface {} for "files" attribute.*`,
   270  		},
   271  		{
   272  			`files: {read: ["/dev/\"quote"]}`,
   273  			`custom-device "read" path must start with / and cannot contain special characters.*`,
   274  		},
   275  		{
   276  			`files: {write: ["/dev/\"quote"]}`,
   277  			`custom-device "write" path must start with / and cannot contain special characters.*`,
   278  		},
   279  		{
   280  			`files: {remove: ["/just/a/file"]}`,
   281  			`cannot specify "remove" in "files" section, only "read" and "write" allowed`,
   282  		},
   283  		{
   284  			`udev-tagging: []`,
   285  			`cannot use custom-device slot without any files or devices`,
   286  		},
   287  		{
   288  			"devices: [/dev/null]\n  udev-tagging: true",
   289  			`snap "provider" has interface "custom-device" with invalid value type bool for "udev-tagging" attribute.*`,
   290  		},
   291  		{
   292  			"devices: [/dev/null]\n  udev-tagging:\n    - foo: bar}",
   293  			`custom-device "udev-tagging" invalid "foo" tag: unknown tag`,
   294  		},
   295  		{
   296  			"devices: [/dev/null]\n  udev-tagging:\n    - subsystem: 12",
   297  			`custom-device "udev-tagging" invalid "subsystem" tag: value "12" is not a string`,
   298  		},
   299  		{
   300  			"devices: [/dev/null]\n  udev-tagging:\n    - subsystem: deal{which,this}",
   301  			`custom-device "udev-tagging" invalid "subsystem" tag: value "deal{which,this}" contains invalid characters`,
   302  		},
   303  		{
   304  			"devices: [/dev/null]\n  udev-tagging:\n    - subsystem: bar",
   305  			`custom-device udev tagging rule missing mandatory "kernel" key`,
   306  		},
   307  		{
   308  			"devices: [/dev/null]\n  udev-tagging:\n    - kernel: bar",
   309  			`custom-device "udev-tagging" invalid "kernel" tag: "bar" does not match a specified device`,
   310  		},
   311  		{
   312  			"devices: [/dev/null]\n  udev-tagging:\n    - attributes: foo",
   313  			`custom-device "udev-tagging" invalid "attributes" tag: value "foo" is not a map`,
   314  		},
   315  		{
   316  			"devices: [/dev/null]\n  udev-tagging:\n    - attributes: {key\": noquotes}",
   317  			`custom-device "udev-tagging" invalid "attributes" tag: key "key"" contains invalid characters`,
   318  		},
   319  		{
   320  			"devices: [/dev/null]\n  udev-tagging:\n    - environment: {key: \"va{ue}\"}",
   321  			`custom-device "udev-tagging" invalid "environment" tag: value "va{ue}" contains invalid characters`,
   322  		},
   323  	}
   324  
   325  	for _, testData := range data {
   326  		snapYaml := fmt.Sprintf(customDeviceYaml, testData.slotYaml)
   327  		_, slot := MockConnectedSlot(c, snapYaml, nil, "hwdev")
   328  		err := interfaces.BeforePrepareSlot(s.iface, slot)
   329  		c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.slotYaml))
   330  	}
   331  }
   332  
   333  func (s *CustomDeviceInterfaceSuite) TestSlotNameAttribute(c *C) {
   334  	var slotYamlTemplate = `name: provider
   335  version: 0
   336  slots:
   337   hwdev:
   338    interface: custom-device
   339    devices: [ /dev/null ]
   340    %s
   341  `
   342  
   343  	data := []struct {
   344  		slotYaml     string
   345  		expectedName string
   346  	}{
   347  		{
   348  			"",      // missing "custom-device" attribute
   349  			"hwdev", // use the name of the slot
   350  		},
   351  		{
   352  			"custom-device: shmemFoo",
   353  			"shmemFoo",
   354  		},
   355  	}
   356  
   357  	for _, testData := range data {
   358  		snapYaml := fmt.Sprintf(slotYamlTemplate, testData.slotYaml)
   359  		_, slot := MockConnectedSlot(c, snapYaml, nil, "hwdev")
   360  		err := interfaces.BeforePrepareSlot(s.iface, slot)
   361  		c.Assert(err, IsNil)
   362  		c.Check(slot.Attrs["custom-device"], Equals, testData.expectedName,
   363  			Commentf(`yaml: %q`, testData.slotYaml))
   364  	}
   365  }
   366  
   367  func (s *CustomDeviceInterfaceSuite) TestStaticInfo(c *C) {
   368  	si := interfaces.StaticInfoOf(s.iface)
   369  	c.Check(si.ImplicitOnCore, Equals, false)
   370  	c.Check(si.ImplicitOnClassic, Equals, false)
   371  	c.Check(si.Summary, Equals, `provides access to custom devices specified via the gadget snap`)
   372  	c.Check(si.BaseDeclarationSlots, testutil.Contains, "custom-device")
   373  }
   374  
   375  func (s *CustomDeviceInterfaceSuite) TestAppArmorSpec(c *C) {
   376  	spec := &apparmor.Specification{}
   377  
   378  	c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
   379  	plugSnippet := spec.SnippetForTag("snap.consumer.app")
   380  
   381  	c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil)
   382  	slotSnippet := spec.SnippetForTag("snap.provider.app")
   383  
   384  	c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
   385  
   386  	c.Check(plugSnippet, testutil.Contains, `"/dev/input/event[0-9]" rw,`)
   387  	c.Check(plugSnippet, testutil.Contains, `"/dev/input/mice" rw,`)
   388  	c.Check(plugSnippet, testutil.Contains, `"/dev/js*" r,`)
   389  	c.Check(plugSnippet, testutil.Contains, `"/bar" rw,`)
   390  	c.Check(plugSnippet, testutil.Contains, `"/dev/input/by-id/*" r,`)
   391  	c.Check(slotSnippet, HasLen, 0)
   392  }
   393  
   394  func (s *CustomDeviceInterfaceSuite) TestUDevSpec(c *C) {
   395  	const slotYamlTemplate = `name: provider
   396  version: 0
   397  slots:
   398   hwdev:
   399    interface: custom-device
   400    custom-device: foo
   401    devices:
   402      - /dev/input/event[0-9]
   403      - /dev/input/mice
   404    read-devices:
   405      - /dev/js*
   406    %s
   407  apps:
   408   app:
   409    slots: [hwdev]
   410  `
   411  
   412  	data := []struct {
   413  		slotYaml      string
   414  		expectedRules []map[string]string
   415  	}{
   416  		{
   417  			"", // missing "udev-tagging" attribute
   418  			[]map[string]string{
   419  				// all rules are automatically-generated
   420  				{`KERNEL`: `"input/event[0-9]"`},
   421  				{`KERNEL`: `"input/mice"`},
   422  				{`KERNEL`: `"js*"`},
   423  			},
   424  		},
   425  		{
   426  			"udev-tagging:\n   - kernel: input/mice\n     subsystem: input",
   427  			[]map[string]string{
   428  				{`KERNEL`: `"input/event[0-9]"`},
   429  				{`KERNEL`: `"input/mice"`, `SUBSYSTEM`: `"input"`},
   430  				{`KERNEL`: `"js*"`},
   431  			},
   432  		},
   433  		{
   434  			`udev-tagging:
   435     - kernel: input/mice
   436       subsystem: input
   437     - kernel: js*
   438       attributes:
   439        attr1: one
   440        attr2: two`,
   441  			[]map[string]string{
   442  				{`KERNEL`: `"input/event[0-9]"`},
   443  				{`KERNEL`: `"input/mice"`, `SUBSYSTEM`: `"input"`},
   444  				{`KERNEL`: `"js*"`, `ATTR{attr1}`: `"one"`, `ATTR{attr2}`: `"two"`},
   445  			},
   446  		},
   447  		{
   448  			`udev-tagging:
   449     - kernel: input/mice
   450       attributes:
   451        wheel: "true"
   452     - kernel: input/event[0-9]
   453       subsystem: input
   454       environment:
   455        env1: first
   456        env2: second|other`,
   457  			[]map[string]string{
   458  				{
   459  					`KERNEL`:    `"input/event[0-9]"`,
   460  					`SUBSYSTEM`: `"input"`,
   461  					`ENV{env1}`: `"first"`,
   462  					`ENV{env2}`: `"second|other"`,
   463  				},
   464  				{`KERNEL`: `"input/mice"`, `ATTR{wheel}`: `"true"`},
   465  				{`KERNEL`: `"js*"`},
   466  			},
   467  		},
   468  	}
   469  
   470  	for _, testData := range data {
   471  		testLabel := Commentf("yaml: %s", testData.slotYaml)
   472  		spec := &udev.Specification{}
   473  		snapYaml := fmt.Sprintf(slotYamlTemplate, testData.slotYaml)
   474  		slot, _ := MockConnectedSlot(c, snapYaml, nil, "hwdev")
   475  		c.Assert(spec.AddConnectedPlug(s.iface, s.plug, slot), IsNil)
   476  		snippets := spec.Snippets()
   477  
   478  		// The first lines are for the tagging, the last one is for the
   479  		// snap-device-helper
   480  		rulesCount := len(testData.expectedRules)
   481  		c.Assert(snippets, HasLen, rulesCount+1)
   482  
   483  		// The following rule is not fixed since the order of the elements depend
   484  		// on the map iteration order, which in golang is not deterministic.
   485  		// Therefore, we decompose each rule into a map:
   486  		var decomposedSnippets []map[string]string
   487  		for _, snippet := range snippets[:rulesCount] {
   488  			lines := strings.Split(snippet, "\n")
   489  			c.Assert(lines, HasLen, 2, testLabel)
   490  
   491  			// The first line is just a comment
   492  			c.Check(lines[0], Matches, "^#.*", testLabel)
   493  
   494  			// The second line contains the actual rule
   495  			ruleTags := strings.Split(lines[1], ", ")
   496  			// Verify that the last part is the tag assignment
   497  			lastElement := len(ruleTags) - 1
   498  			c.Check(ruleTags[lastElement], Equals, `TAG+="snap_consumer_app"`)
   499  			decomposedTags := make(map[string]string)
   500  			for _, ruleTag := range ruleTags[:lastElement] {
   501  				tagMembers := strings.SplitN(ruleTag, "==", 2)
   502  				c.Assert(tagMembers, HasLen, 2)
   503  				decomposedTags[tagMembers[0]] = tagMembers[1]
   504  			}
   505  			decomposedSnippets = append(decomposedSnippets, decomposedTags)
   506  		}
   507  		c.Assert(decomposedSnippets, testutil.DeepUnsortedMatches, testData.expectedRules, testLabel)
   508  
   509  		// The last line of the snippet is about snap-device-helper
   510  		actionLine := snippets[rulesCount]
   511  		c.Assert(actionLine, Matches,
   512  			fmt.Sprintf(`^TAG=="snap_consumer_app", RUN\+="%s/snap-device-helper .*`, dirs.DistroLibExecDir),
   513  			testLabel)
   514  	}
   515  }
   516  
   517  func (s *CustomDeviceInterfaceSuite) TestAutoConnect(c *C) {
   518  	c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true)
   519  }
   520  
   521  func (s *CustomDeviceInterfaceSuite) TestInterfaces(c *C) {
   522  	c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
   523  }