github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 type disconnectOpts struct { 325 AutoDisconnect bool 326 ByHotplug bool 327 } 328 329 // disconnectTasks creates a set of tasks for disconnect, including hooks, but does not do any conflict checking. 330 func disconnectTasks(st *state.State, conn *interfaces.Connection, flags disconnectOpts) (*state.TaskSet, error) { 331 plugSnap := conn.Plug.Snap().InstanceName() 332 slotSnap := conn.Slot.Snap().InstanceName() 333 plugName := conn.Plug.Name() 334 slotName := conn.Slot.Name() 335 336 var plugSnapst, slotSnapst snapstate.SnapState 337 if err := snapstate.Get(st, slotSnap, &slotSnapst); err != nil { 338 return nil, err 339 } 340 if err := snapstate.Get(st, plugSnap, &plugSnapst); err != nil { 341 return nil, err 342 } 343 344 summary := fmt.Sprintf(i18n.G("Disconnect %s:%s from %s:%s"), 345 plugSnap, plugName, slotSnap, slotName) 346 disconnectTask := st.NewTask("disconnect", summary) 347 disconnectTask.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) 348 disconnectTask.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) 349 350 disconnectTask.Set("slot-static", conn.Slot.StaticAttrs()) 351 disconnectTask.Set("slot-dynamic", conn.Slot.DynamicAttrs()) 352 disconnectTask.Set("plug-static", conn.Plug.StaticAttrs()) 353 disconnectTask.Set("plug-dynamic", conn.Plug.DynamicAttrs()) 354 355 if flags.AutoDisconnect { 356 disconnectTask.Set("auto-disconnect", true) 357 } 358 if flags.ByHotplug { 359 disconnectTask.Set("by-hotplug", true) 360 } 361 362 ts := state.NewTaskSet() 363 var prev *state.Task 364 addTask := func(t *state.Task) { 365 if prev != nil { 366 t.WaitFor(prev) 367 } 368 ts.AddTask(t) 369 prev = t 370 } 371 372 initialContext := make(map[string]interface{}) 373 initialContext["attrs-task"] = disconnectTask.ID() 374 375 plugSnapInfo, err := plugSnapst.CurrentInfo() 376 if err != nil { 377 return nil, err 378 } 379 slotSnapInfo, err := slotSnapst.CurrentInfo() 380 if err != nil { 381 return nil, err 382 } 383 384 // only run slot hooks if slotSnap is active 385 if slotSnapst.Active { 386 hookName := fmt.Sprintf("disconnect-slot-%s", slotName) 387 if slotSnapInfo.Hooks[hookName] != nil { 388 disconnectSlotHookSetup := &hookstate.HookSetup{ 389 Snap: slotSnap, 390 Hook: hookName, 391 Optional: true, 392 } 393 undoDisconnectSlotHookSetup := &hookstate.HookSetup{ 394 Snap: slotSnap, 395 Hook: "connect-slot-" + slotName, 396 Optional: true, 397 } 398 399 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectSlotHookSetup.Hook, disconnectSlotHookSetup.Snap) 400 disconnectSlot := hookstate.HookTaskWithUndo(st, summary, disconnectSlotHookSetup, undoDisconnectSlotHookSetup, initialContext) 401 402 addTask(disconnectSlot) 403 } 404 } 405 406 // only run plug hooks if plugSnap is active 407 if plugSnapst.Active { 408 hookName := fmt.Sprintf("disconnect-plug-%s", plugName) 409 if plugSnapInfo.Hooks[hookName] != nil { 410 disconnectPlugHookSetup := &hookstate.HookSetup{ 411 Snap: plugSnap, 412 Hook: hookName, 413 Optional: true, 414 } 415 undoDisconnectPlugHookSetup := &hookstate.HookSetup{ 416 Snap: plugSnap, 417 Hook: "connect-plug-" + plugName, 418 Optional: true, 419 } 420 421 summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectPlugHookSetup.Hook, disconnectPlugHookSetup.Snap) 422 disconnectPlug := hookstate.HookTaskWithUndo(st, summary, disconnectPlugHookSetup, undoDisconnectPlugHookSetup, initialContext) 423 424 addTask(disconnectPlug) 425 } 426 } 427 428 addTask(disconnectTask) 429 return ts, nil 430 } 431 432 // CheckInterfaces checks whether plugs and slots of snap are allowed for installation. 433 func CheckInterfaces(st *state.State, snapInfo *snap.Info, deviceCtx snapstate.DeviceContext) error { 434 // XXX: addImplicitSlots is really a brittle interface 435 if err := addImplicitSlots(st, snapInfo); err != nil { 436 return err 437 } 438 439 modelAs := deviceCtx.Model() 440 441 var storeAs *asserts.Store 442 if modelAs.Store() != "" { 443 var err error 444 storeAs, err = assertstate.Store(st, modelAs.Store()) 445 if err != nil && !asserts.IsNotFound(err) { 446 return err 447 } 448 } 449 450 baseDecl, err := assertstate.BaseDeclaration(st) 451 if err != nil { 452 return fmt.Errorf("internal error: cannot find base declaration: %v", err) 453 } 454 455 if snapInfo.SnapID == "" { 456 // no SnapID means --dangerous was given, perform a minimal check about the compatibility of the snap type and the interface 457 ic := policy.InstallCandidateMinimalCheck{ 458 Snap: snapInfo, 459 BaseDeclaration: baseDecl, 460 Model: modelAs, 461 Store: storeAs, 462 } 463 return ic.Check() 464 } 465 466 snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID) 467 if err != nil { 468 return fmt.Errorf("cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err) 469 } 470 471 ic := policy.InstallCandidate{ 472 Snap: snapInfo, 473 SnapDeclaration: snapDecl, 474 BaseDeclaration: baseDecl, 475 Model: modelAs, 476 Store: storeAs, 477 } 478 479 return ic.Check() 480 } 481 482 var once sync.Once 483 484 func delayedCrossMgrInit() { 485 once.Do(func() { 486 // hook interface checks into snapstate installation logic 487 488 snapstate.AddCheckSnapCallback(func(st *state.State, snapInfo, _ *snap.Info, _ snapstate.Flags, deviceCtx snapstate.DeviceContext) error { 489 return CheckInterfaces(st, snapInfo, deviceCtx) 490 }) 491 492 // hook into conflict checks mechanisms 493 snapstate.AddAffectedSnapsByKind("connect", connectDisconnectAffectedSnaps) 494 snapstate.AddAffectedSnapsByKind("disconnect", connectDisconnectAffectedSnaps) 495 }) 496 } 497 498 func MockConnectRetryTimeout(d time.Duration) (restore func()) { 499 old := connectRetryTimeout 500 connectRetryTimeout = d 501 return func() { connectRetryTimeout = old } 502 }