github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/ifacestate/ifacestate.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2017 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 ifacestate implements the manager and state aspects 21 // responsible for the maintenance of interfaces the system. 22 package ifacestate 23 24 import ( 25 "fmt" 26 "sync" 27 "time" 28 29 "github.com/snapcore/snapd/asserts" 30 "github.com/snapcore/snapd/i18n" 31 "github.com/snapcore/snapd/interfaces" 32 "github.com/snapcore/snapd/interfaces/policy" 33 "github.com/snapcore/snapd/overlord/assertstate" 34 "github.com/snapcore/snapd/overlord/hookstate" 35 "github.com/snapcore/snapd/overlord/snapstate" 36 "github.com/snapcore/snapd/overlord/state" 37 "github.com/snapcore/snapd/snap" 38 ) 39 40 var connectRetryTimeout = time.Second * 5 41 42 // ErrAlreadyConnected describes the error that occurs when attempting to connect already connected interface. 43 type ErrAlreadyConnected struct { 44 Connection interfaces.ConnRef 45 } 46 47 const ( 48 ConnectTaskEdge = state.TaskSetEdge("connect-task") 49 AfterConnectHooksEdge = state.TaskSetEdge("after-connect-hooks") 50 ) 51 52 func (e ErrAlreadyConnected) Error() string { 53 return fmt.Sprintf("already connected: %q", e.Connection.ID()) 54 } 55 56 // findSymmetricAutoconnectTask checks if there is another auto-connect task affecting same snap because of plug/slot. 57 func findSymmetricAutoconnectTask(st *state.State, plugSnap, slotSnap string, installTask *state.Task) (bool, error) { 58 snapsup, err := snapstate.TaskSnapSetup(installTask) 59 if err != nil { 60 return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", installTask.Summary()) 61 } 62 installedSnap := snapsup.InstanceName() 63 64 // if we find any auto-connect task that's not ready and is affecting our snap, return true to indicate that 65 // it should be ignored (we shouldn't create connect tasks for it) 66 for _, task := range st.Tasks() { 67 if !task.Status().Ready() && task.ID() != installTask.ID() && task.Kind() == "auto-connect" { 68 snapsup, err := snapstate.TaskSnapSetup(task) 69 if err != nil { 70 return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", task.Summary()) 71 } 72 otherSnap := snapsup.InstanceName() 73 74 if (otherSnap == plugSnap && installedSnap == slotSnap) || (otherSnap == slotSnap && installedSnap == plugSnap) { 75 return true, nil 76 } 77 } 78 } 79 return false, nil 80 } 81 82 type connectOpts struct { 83 ByGadget bool 84 AutoConnect bool 85 86 DelayedSetupProfiles bool 87 } 88 89 // Connect returns a set of tasks for connecting an interface. 90 // 91 func Connect(st *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { 92 if err := snapstate.CheckChangeConflictMany(st, []string{plugSnap, slotSnap}, ""); err != nil { 93 return nil, err 94 } 95 96 return connect(st, plugSnap, plugName, slotSnap, slotName, connectOpts{}) 97 } 98 99 func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, flags connectOpts) (*state.TaskSet, error) { 100 // TODO: Store the intent-to-connect in the state so that we automatically 101 // try to reconnect on reboot (reconnection can fail or can connect with 102 // different parameters so we cannot store the actual connection details). 103 104 // Create a series of tasks: 105 // - prepare-plug-<plug> hook 106 // - prepare-slot-<slot> hook 107 // - connect task 108 // - connect-slot-<slot> hook 109 // - connect-plug-<plug> hook 110 // The tasks run in sequence (are serialized by WaitFor). The hooks are optional 111 // and their tasks are created when hook exists or is declared in the snap. 112 // The prepare- hooks collect attributes via snapctl set. 113 // 'snapctl set' can only modify own attributes (plug's attributes in the *-plug-* hook and 114 // slot's attributes in the *-slot-* hook). 115 // 'snapctl get' can read both slot's and plug's attributes. 116 117 // check if the connection already exists 118 conns, err := getConns(st) 119 if err != nil { 120 return nil, err 121 } 122 connRef := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plugName}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slotName}} 123 if conn, ok := conns[connRef.ID()]; ok && conn.Undesired == false && conn.HotplugGone == false { 124 return nil, &ErrAlreadyConnected{Connection: connRef} 125 } 126 127 var plugSnapst, slotSnapst snapstate.SnapState 128 if err = snapstate.Get(st, plugSnap, &plugSnapst); err != nil { 129 return nil, err 130 } 131 if err = snapstate.Get(st, slotSnap, &slotSnapst); err != nil { 132 return nil, err 133 } 134 plugSnapInfo, err := plugSnapst.CurrentInfo() 135 if err != nil { 136 return nil, err 137 } 138 slotSnapInfo, err := slotSnapst.CurrentInfo() 139 if err != nil { 140 return nil, err 141 } 142 143 plugStatic, slotStatic, err := initialConnectAttributes(st, plugSnapInfo, plugSnap, plugName, slotSnapInfo, slotSnap, slotName) 144 if err != nil { 145 return nil, err 146 } 147 148 connectInterface := st.NewTask("connect", fmt.Sprintf(i18n.G("Connect %s:%s to %s:%s"), plugSnap, plugName, slotSnap, slotName)) 149 initialContext := make(map[string]interface{}) 150 initialContext["attrs-task"] = connectInterface.ID() 151 152 tasks := state.NewTaskSet() 153 var prev *state.Task 154 addTask := func(t *state.Task) { 155 if prev != nil { 156 t.WaitFor(prev) 157 } 158 tasks.AddTask(t) 159 } 160 161 preparePlugHookName := fmt.Sprintf("prepare-plug-%s", plugName) 162 if plugSnapInfo.Hooks[preparePlugHookName] != nil { 163 plugHookSetup := &hookstate.HookSetup{ 164 Snap: plugSnap, 165 Hook: preparePlugHookName, 166 Optional: true, 167 } 168 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), plugHookSetup.Hook, plugHookSetup.Snap) 169 undoPrepPlugHookSetup := &hookstate.HookSetup{ 170 Snap: plugSnap, 171 Hook: "unprepare-plug-" + plugName, 172 Optional: true, 173 IgnoreError: true, 174 } 175 preparePlugConnection := hookstate.HookTaskWithUndo(st, summary, plugHookSetup, undoPrepPlugHookSetup, initialContext) 176 addTask(preparePlugConnection) 177 prev = preparePlugConnection 178 } 179 180 prepareSlotHookName := fmt.Sprintf("prepare-slot-%s", slotName) 181 if slotSnapInfo.Hooks[prepareSlotHookName] != nil { 182 slotHookSetup := &hookstate.HookSetup{ 183 Snap: slotSnap, 184 Hook: prepareSlotHookName, 185 Optional: true, 186 } 187 undoPrepSlotHookSetup := &hookstate.HookSetup{ 188 Snap: slotSnap, 189 Hook: "unprepare-slot-" + slotName, 190 Optional: true, 191 IgnoreError: true, 192 } 193 194 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), slotHookSetup.Hook, slotHookSetup.Snap) 195 prepareSlotConnection := hookstate.HookTaskWithUndo(st, summary, slotHookSetup, undoPrepSlotHookSetup, initialContext) 196 addTask(prepareSlotConnection) 197 prev = prepareSlotConnection 198 } 199 200 connectInterface.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) 201 connectInterface.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) 202 if flags.AutoConnect { 203 connectInterface.Set("auto", true) 204 } 205 if flags.ByGadget { 206 connectInterface.Set("by-gadget", true) 207 } 208 if flags.DelayedSetupProfiles { 209 connectInterface.Set("delayed-setup-profiles", true) 210 } 211 212 // Expose a copy of all plug and slot attributes coming from yaml to interface hooks. The hooks will be able 213 // to modify them but all attributes will be checked against assertions after the hooks are run. 214 emptyDynamicAttrs := map[string]interface{}{} 215 connectInterface.Set("plug-static", plugStatic) 216 connectInterface.Set("slot-static", slotStatic) 217 connectInterface.Set("plug-dynamic", emptyDynamicAttrs) 218 connectInterface.Set("slot-dynamic", emptyDynamicAttrs) 219 220 // The main 'connect' task should wait on prepare-slot- hook or on prepare-plug- hook (whichever is present), 221 // but not on both. While there would be no harm in waiting for both, it's not needed as prepare-slot- will 222 // wait for prepare-plug- anyway, and a simple one-to-one wait dependency makes testing easier. 223 addTask(connectInterface) 224 prev = connectInterface 225 226 if flags.DelayedSetupProfiles { 227 // mark as the last task in connect prepare 228 tasks.MarkEdge(connectInterface, ConnectTaskEdge) 229 } 230 231 connectSlotHookName := fmt.Sprintf("connect-slot-%s", slotName) 232 if slotSnapInfo.Hooks[connectSlotHookName] != nil { 233 connectSlotHookSetup := &hookstate.HookSetup{ 234 Snap: slotSnap, 235 Hook: connectSlotHookName, 236 Optional: true, 237 } 238 undoConnectSlotHookSetup := &hookstate.HookSetup{ 239 Snap: slotSnap, 240 Hook: "disconnect-slot-" + slotName, 241 Optional: true, 242 IgnoreError: true, 243 } 244 245 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectSlotHookSetup.Hook, connectSlotHookSetup.Snap) 246 connectSlotConnection := hookstate.HookTaskWithUndo(st, summary, connectSlotHookSetup, undoConnectSlotHookSetup, initialContext) 247 addTask(connectSlotConnection) 248 prev = connectSlotConnection 249 if flags.DelayedSetupProfiles { 250 tasks.MarkEdge(connectSlotConnection, AfterConnectHooksEdge) 251 } 252 } 253 254 connectPlugHookName := fmt.Sprintf("connect-plug-%s", plugName) 255 if plugSnapInfo.Hooks[connectPlugHookName] != nil { 256 connectPlugHookSetup := &hookstate.HookSetup{ 257 Snap: plugSnap, 258 Hook: connectPlugHookName, 259 Optional: true, 260 } 261 undoConnectPlugHookSetup := &hookstate.HookSetup{ 262 Snap: plugSnap, 263 Hook: "disconnect-plug-" + plugName, 264 Optional: true, 265 IgnoreError: true, 266 } 267 268 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectPlugHookSetup.Hook, connectPlugHookSetup.Snap) 269 connectPlugConnection := hookstate.HookTaskWithUndo(st, summary, connectPlugHookSetup, undoConnectPlugHookSetup, initialContext) 270 addTask(connectPlugConnection) 271 272 if flags.DelayedSetupProfiles { 273 // only mark AfterConnectHooksEdge if not already set on connect-slot- hook task 274 if edge, _ := tasks.Edge(AfterConnectHooksEdge); edge == nil { 275 tasks.MarkEdge(connectPlugConnection, AfterConnectHooksEdge) 276 } 277 } 278 prev = connectPlugConnection 279 } 280 return tasks, nil 281 } 282 283 func initialConnectAttributes(st *state.State, plugSnapInfo *snap.Info, plugSnap string, plugName string, slotSnapInfo *snap.Info, slotSnap string, slotName string) (plugStatic, slotStatic map[string]interface{}, err error) { 284 var plugSnapst snapstate.SnapState 285 286 if err = snapstate.Get(st, plugSnap, &plugSnapst); err != nil { 287 return nil, nil, err 288 } 289 290 plug, ok := plugSnapInfo.Plugs[plugName] 291 if !ok { 292 return nil, nil, fmt.Errorf("snap %q has no plug named %q", plugSnap, plugName) 293 } 294 295 var slotSnapst snapstate.SnapState 296 297 if err = snapstate.Get(st, slotSnap, &slotSnapst); err != nil { 298 return nil, nil, err 299 } 300 301 if err := addImplicitSlots(st, slotSnapInfo); err != nil { 302 return nil, nil, err 303 } 304 305 slot, ok := slotSnapInfo.Slots[slotName] 306 if !ok { 307 return nil, nil, fmt.Errorf("snap %q has no slot named %q", slotSnap, slotName) 308 } 309 310 return plug.Attrs, slot.Attrs, nil 311 } 312 313 // Disconnect returns a set of tasks for disconnecting an interface. 314 func Disconnect(st *state.State, conn *interfaces.Connection) (*state.TaskSet, error) { 315 plugSnap := conn.Plug.Snap().InstanceName() 316 slotSnap := conn.Slot.Snap().InstanceName() 317 if err := snapstate.CheckChangeConflictMany(st, []string{plugSnap, slotSnap}, ""); err != nil { 318 return nil, err 319 } 320 321 return disconnectTasks(st, conn, disconnectOpts{}) 322 } 323 324 // Forget returs a set of tasks for disconnecting and forgetting an interface. 325 // If the interface is already disconnected, it will be removed from the state 326 // (forgotten). 327 func Forget(st *state.State, repo *interfaces.Repository, connRef *interfaces.ConnRef) (*state.TaskSet, error) { 328 if err := snapstate.CheckChangeConflictMany(st, []string{connRef.PlugRef.Snap, connRef.SlotRef.Snap}, ""); err != nil { 329 return nil, err 330 } 331 332 if conn, err := repo.Connection(connRef); err == nil { 333 // connection exists - run regular set of disconnect tasks with forget 334 // flag. 335 opts := disconnectOpts{Forget: true} 336 ts, err := disconnectTasks(st, conn, opts) 337 return ts, err 338 } 339 340 // connection is not active (and possibly either the plug or slot 341 // doesn't exist); disconnect tasks don't need hooks as we simply 342 // want to remove connection from state. 343 ts := forgetTasks(st, connRef) 344 return ts, nil 345 } 346 347 type disconnectOpts struct { 348 AutoDisconnect bool 349 ByHotplug bool 350 Forget bool 351 } 352 353 // forgetTasks creates a set of tasks for forgetting an inactive connection 354 func forgetTasks(st *state.State, connRef *interfaces.ConnRef) *state.TaskSet { 355 summary := fmt.Sprintf(i18n.G("Forget connection %s:%s from %s:%s"), 356 connRef.PlugRef.Snap, connRef.PlugRef.Name, 357 connRef.SlotRef.Snap, connRef.SlotRef.Name) 358 disconnectTask := st.NewTask("disconnect", summary) 359 disconnectTask.Set("slot", connRef.SlotRef) 360 disconnectTask.Set("plug", connRef.PlugRef) 361 disconnectTask.Set("forget", true) 362 return state.NewTaskSet(disconnectTask) 363 } 364 365 // disconnectTasks creates a set of tasks for disconnect, including hooks, but does not do any conflict checking. 366 func disconnectTasks(st *state.State, conn *interfaces.Connection, flags disconnectOpts) (*state.TaskSet, error) { 367 plugSnap := conn.Plug.Snap().InstanceName() 368 slotSnap := conn.Slot.Snap().InstanceName() 369 plugName := conn.Plug.Name() 370 slotName := conn.Slot.Name() 371 372 var plugSnapst, slotSnapst snapstate.SnapState 373 if err := snapstate.Get(st, slotSnap, &slotSnapst); err != nil { 374 return nil, err 375 } 376 if err := snapstate.Get(st, plugSnap, &plugSnapst); err != nil { 377 return nil, err 378 } 379 380 summary := fmt.Sprintf(i18n.G("Disconnect %s:%s from %s:%s"), 381 plugSnap, plugName, slotSnap, slotName) 382 disconnectTask := st.NewTask("disconnect", summary) 383 disconnectTask.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) 384 disconnectTask.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) 385 if flags.Forget { 386 disconnectTask.Set("forget", true) 387 } 388 389 disconnectTask.Set("slot-static", conn.Slot.StaticAttrs()) 390 disconnectTask.Set("slot-dynamic", conn.Slot.DynamicAttrs()) 391 disconnectTask.Set("plug-static", conn.Plug.StaticAttrs()) 392 disconnectTask.Set("plug-dynamic", conn.Plug.DynamicAttrs()) 393 394 if flags.AutoDisconnect { 395 disconnectTask.Set("auto-disconnect", true) 396 } 397 if flags.ByHotplug { 398 disconnectTask.Set("by-hotplug", true) 399 } 400 401 ts := state.NewTaskSet() 402 var prev *state.Task 403 addTask := func(t *state.Task) { 404 if prev != nil { 405 t.WaitFor(prev) 406 } 407 ts.AddTask(t) 408 prev = t 409 } 410 411 initialContext := make(map[string]interface{}) 412 initialContext["attrs-task"] = disconnectTask.ID() 413 414 plugSnapInfo, err := plugSnapst.CurrentInfo() 415 if err != nil { 416 return nil, err 417 } 418 slotSnapInfo, err := slotSnapst.CurrentInfo() 419 if err != nil { 420 return nil, err 421 } 422 423 // only run slot hooks if slotSnap is active 424 if slotSnapst.Active { 425 hookName := fmt.Sprintf("disconnect-slot-%s", slotName) 426 if slotSnapInfo.Hooks[hookName] != nil { 427 disconnectSlotHookSetup := &hookstate.HookSetup{ 428 Snap: slotSnap, 429 Hook: hookName, 430 Optional: true, 431 } 432 undoDisconnectSlotHookSetup := &hookstate.HookSetup{ 433 Snap: slotSnap, 434 Hook: "connect-slot-" + slotName, 435 Optional: true, 436 } 437 438 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectSlotHookSetup.Hook, disconnectSlotHookSetup.Snap) 439 disconnectSlot := hookstate.HookTaskWithUndo(st, summary, disconnectSlotHookSetup, undoDisconnectSlotHookSetup, initialContext) 440 441 addTask(disconnectSlot) 442 } 443 } 444 445 // only run plug hooks if plugSnap is active 446 if plugSnapst.Active { 447 hookName := fmt.Sprintf("disconnect-plug-%s", plugName) 448 if plugSnapInfo.Hooks[hookName] != nil { 449 disconnectPlugHookSetup := &hookstate.HookSetup{ 450 Snap: plugSnap, 451 Hook: hookName, 452 Optional: true, 453 } 454 undoDisconnectPlugHookSetup := &hookstate.HookSetup{ 455 Snap: plugSnap, 456 Hook: "connect-plug-" + plugName, 457 Optional: true, 458 } 459 460 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectPlugHookSetup.Hook, disconnectPlugHookSetup.Snap) 461 disconnectPlug := hookstate.HookTaskWithUndo(st, summary, disconnectPlugHookSetup, undoDisconnectPlugHookSetup, initialContext) 462 463 addTask(disconnectPlug) 464 } 465 } 466 467 addTask(disconnectTask) 468 return ts, nil 469 } 470 471 // CheckInterfaces checks whether plugs and slots of snap are allowed for installation. 472 func CheckInterfaces(st *state.State, snapInfo *snap.Info, deviceCtx snapstate.DeviceContext) error { 473 // XXX: addImplicitSlots is really a brittle interface 474 if err := addImplicitSlots(st, snapInfo); err != nil { 475 return err 476 } 477 478 modelAs := deviceCtx.Model() 479 480 var storeAs *asserts.Store 481 if modelAs.Store() != "" { 482 var err error 483 storeAs, err = assertstate.Store(st, modelAs.Store()) 484 if err != nil && !asserts.IsNotFound(err) { 485 return err 486 } 487 } 488 489 baseDecl, err := assertstate.BaseDeclaration(st) 490 if err != nil { 491 return fmt.Errorf("internal error: cannot find base declaration: %v", err) 492 } 493 494 if snapInfo.SnapID == "" { 495 // no SnapID means --dangerous was given, perform a minimal check about the compatibility of the snap type and the interface 496 ic := policy.InstallCandidateMinimalCheck{ 497 Snap: snapInfo, 498 BaseDeclaration: baseDecl, 499 Model: modelAs, 500 Store: storeAs, 501 } 502 return ic.Check() 503 } 504 505 snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID) 506 if err != nil { 507 return fmt.Errorf("cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err) 508 } 509 510 ic := policy.InstallCandidate{ 511 Snap: snapInfo, 512 SnapDeclaration: snapDecl, 513 BaseDeclaration: baseDecl, 514 Model: modelAs, 515 Store: storeAs, 516 } 517 518 return ic.Check() 519 } 520 521 var once sync.Once 522 523 func delayedCrossMgrInit() { 524 once.Do(func() { 525 // hook interface checks into snapstate installation logic 526 527 snapstate.AddCheckSnapCallback(func(st *state.State, snapInfo, _ *snap.Info, _ snap.Container, _ snapstate.Flags, deviceCtx snapstate.DeviceContext) error { 528 return CheckInterfaces(st, snapInfo, deviceCtx) 529 }) 530 531 // hook into conflict checks mechanisms 532 snapstate.AddAffectedSnapsByKind("connect", connectDisconnectAffectedSnaps) 533 snapstate.AddAffectedSnapsByKind("disconnect", connectDisconnectAffectedSnaps) 534 }) 535 } 536 537 func MockConnectRetryTimeout(d time.Duration) (restore func()) { 538 old := connectRetryTimeout 539 connectRetryTimeout = d 540 return func() { connectRetryTimeout = old } 541 }