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 }