gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/daemon/api_interfaces.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 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 daemon
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"net/http"
    26  	"sort"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/interfaces"
    30  	"github.com/snapcore/snapd/overlord/auth"
    31  	"github.com/snapcore/snapd/overlord/ifacestate"
    32  	"github.com/snapcore/snapd/overlord/snapstate"
    33  	"github.com/snapcore/snapd/overlord/state"
    34  )
    35  
    36  var (
    37  	interfacesCmd = &Command{
    38  		Path:        "/v2/interfaces",
    39  		GET:         interfacesConnectionsMultiplexer,
    40  		POST:        changeInterfaces,
    41  		ReadAccess:  openAccess{},
    42  		WriteAccess: authenticatedAccess{Polkit: polkitActionManageInterfaces},
    43  	}
    44  )
    45  
    46  // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces).
    47  func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response {
    48  	query := r.URL.Query()
    49  	qselect := query.Get("select")
    50  	if qselect == "" {
    51  		return getLegacyConnections(c, r, user)
    52  	} else {
    53  		return getInterfaces(c, r, user)
    54  	}
    55  }
    56  
    57  func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
    58  	// Collect query options from request arguments.
    59  	q := r.URL.Query()
    60  	pselect := q.Get("select")
    61  	if pselect != "all" && pselect != "connected" {
    62  		return BadRequest("unsupported select qualifier")
    63  	}
    64  	var names []string // Interface names
    65  	namesStr := q.Get("names")
    66  	if namesStr != "" {
    67  		names = strings.Split(namesStr, ",")
    68  	}
    69  	opts := &interfaces.InfoOptions{
    70  		Names:     names,
    71  		Doc:       q.Get("doc") == "true",
    72  		Plugs:     q.Get("plugs") == "true",
    73  		Slots:     q.Get("slots") == "true",
    74  		Connected: pselect == "connected",
    75  	}
    76  	// Query the interface repository (this returns []*interface.Info).
    77  	infos := c.d.overlord.InterfaceManager().Repository().Info(opts)
    78  	infoJSONs := make([]*interfaceJSON, 0, len(infos))
    79  
    80  	for _, info := range infos {
    81  		// Convert interfaces.Info into interfaceJSON
    82  		plugs := make([]*plugJSON, 0, len(info.Plugs))
    83  		for _, plug := range info.Plugs {
    84  			plugs = append(plugs, &plugJSON{
    85  				Snap:  plug.Snap.InstanceName(),
    86  				Name:  plug.Name,
    87  				Attrs: plug.Attrs,
    88  				Label: plug.Label,
    89  			})
    90  		}
    91  		slots := make([]*slotJSON, 0, len(info.Slots))
    92  		for _, slot := range info.Slots {
    93  			slots = append(slots, &slotJSON{
    94  				Snap:  slot.Snap.InstanceName(),
    95  				Name:  slot.Name,
    96  				Attrs: slot.Attrs,
    97  				Label: slot.Label,
    98  			})
    99  		}
   100  		infoJSONs = append(infoJSONs, &interfaceJSON{
   101  			Name:    info.Name,
   102  			Summary: info.Summary,
   103  			DocURL:  info.DocURL,
   104  			Plugs:   plugs,
   105  			Slots:   slots,
   106  		})
   107  	}
   108  	return SyncResponse(infoJSONs)
   109  }
   110  
   111  func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response {
   112  	connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{})
   113  	if err != nil {
   114  		return InternalError("collecting connection information failed: %v", err)
   115  	}
   116  	legacyconnsjson := legacyConnectionsJSON{
   117  		Plugs: connsjson.Plugs,
   118  		Slots: connsjson.Slots,
   119  	}
   120  	return SyncResponse(legacyconnsjson)
   121  }
   122  
   123  // changeInterfaces controls the interfaces system.
   124  // Plugs can be connected to and disconnected from slots.
   125  func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
   126  	var a interfaceAction
   127  	decoder := json.NewDecoder(r.Body)
   128  	if err := decoder.Decode(&a); err != nil {
   129  		return BadRequest("cannot decode request body into an interface action: %v", err)
   130  	}
   131  	if a.Action == "" {
   132  		return BadRequest("interface action not specified")
   133  	}
   134  	if len(a.Plugs) > 1 || len(a.Slots) > 1 {
   135  		return NotImplemented("many-to-many operations are not implemented")
   136  	}
   137  	if a.Action != "connect" && a.Action != "disconnect" {
   138  		return BadRequest("unsupported interface action: %q", a.Action)
   139  	}
   140  	if len(a.Plugs) == 0 || len(a.Slots) == 0 {
   141  		return BadRequest("at least one plug and slot is required")
   142  	}
   143  
   144  	var summary string
   145  	var err error
   146  
   147  	var tasksets []*state.TaskSet
   148  	var affected []string
   149  
   150  	st := c.d.overlord.State()
   151  	st.Lock()
   152  	defer st.Unlock()
   153  
   154  	checkInstalled := func(snapName string) error {
   155  		// empty snap name is fine, ResolveConnect/ResolveDisconnect handles it.
   156  		if snapName == "" {
   157  			return nil
   158  		}
   159  		var snapst snapstate.SnapState
   160  		err := snapstate.Get(st, snapName, &snapst)
   161  		if (err == nil && !snapst.IsInstalled()) || err == state.ErrNoState {
   162  			return fmt.Errorf("snap %q is not installed", snapName)
   163  		}
   164  		if err == nil {
   165  			return nil
   166  		}
   167  		return fmt.Errorf("internal error: cannot get state of snap %q: %v", snapName, err)
   168  	}
   169  
   170  	for i := range a.Plugs {
   171  		a.Plugs[i].Snap = ifacestate.RemapSnapFromRequest(a.Plugs[i].Snap)
   172  		if err := checkInstalled(a.Plugs[i].Snap); err != nil {
   173  			return errToResponse(err, nil, BadRequest, "%v")
   174  		}
   175  	}
   176  	for i := range a.Slots {
   177  		a.Slots[i].Snap = ifacestate.RemapSnapFromRequest(a.Slots[i].Snap)
   178  		if err := checkInstalled(a.Slots[i].Snap); err != nil {
   179  			return errToResponse(err, nil, BadRequest, "%v")
   180  		}
   181  	}
   182  
   183  	switch a.Action {
   184  	case "connect":
   185  		var connRef *interfaces.ConnRef
   186  		repo := c.d.overlord.InterfaceManager().Repository()
   187  		connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
   188  		if err == nil {
   189  			var ts *state.TaskSet
   190  			affected = snapNamesFromConns([]*interfaces.ConnRef{connRef})
   191  			summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
   192  			ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
   193  			if _, ok := err.(*ifacestate.ErrAlreadyConnected); ok {
   194  				change := newChange(st, a.Action+"-snap", summary, nil, affected)
   195  				change.SetStatus(state.DoneStatus)
   196  				return AsyncResponse(nil, change.ID())
   197  			}
   198  			tasksets = append(tasksets, ts)
   199  		}
   200  	case "disconnect":
   201  		var conns []*interfaces.ConnRef
   202  		summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
   203  		conns, err = c.d.overlord.InterfaceManager().ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name, a.Forget)
   204  		if err == nil {
   205  			if len(conns) == 0 {
   206  				return InterfacesUnchanged("nothing to do")
   207  			}
   208  			repo := c.d.overlord.InterfaceManager().Repository()
   209  			for _, connRef := range conns {
   210  				var ts *state.TaskSet
   211  				var conn *interfaces.Connection
   212  				if a.Forget {
   213  					ts, err = ifacestate.Forget(st, repo, connRef)
   214  				} else {
   215  					conn, err = repo.Connection(connRef)
   216  					if err != nil {
   217  						break
   218  					}
   219  					ts, err = ifacestate.Disconnect(st, conn)
   220  					if err != nil {
   221  						break
   222  					}
   223  				}
   224  				if err != nil {
   225  					break
   226  				}
   227  				ts.JoinLane(st.NewLane())
   228  				tasksets = append(tasksets, ts)
   229  			}
   230  			affected = snapNamesFromConns(conns)
   231  		}
   232  	}
   233  	if err != nil {
   234  		return errToResponse(err, nil, BadRequest, "%v")
   235  	}
   236  
   237  	change := newChange(st, a.Action+"-snap", summary, tasksets, affected)
   238  	st.EnsureBefore(0)
   239  
   240  	return AsyncResponse(nil, change.ID())
   241  }
   242  
   243  func snapNamesFromConns(conns []*interfaces.ConnRef) []string {
   244  	m := make(map[string]bool)
   245  	for _, conn := range conns {
   246  		m[conn.PlugRef.Snap] = true
   247  		m[conn.SlotRef.Snap] = true
   248  	}
   249  	l := make([]string, 0, len(m))
   250  	for name := range m {
   251  		l = append(l, name)
   252  	}
   253  	sort.Strings(l)
   254  	return l
   255  }