github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_quotas.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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  	"net/http"
    25  	"sort"
    26  
    27  	"github.com/snapcore/snapd/client"
    28  	"github.com/snapcore/snapd/gadget/quantity"
    29  	"github.com/snapcore/snapd/overlord/auth"
    30  	"github.com/snapcore/snapd/overlord/servicestate"
    31  	"github.com/snapcore/snapd/snap/naming"
    32  	"github.com/snapcore/snapd/snap/quota"
    33  )
    34  
    35  var (
    36  	quotaGroupsCmd = &Command{
    37  		Path:        "/v2/quotas",
    38  		GET:         getQuotaGroups,
    39  		POST:        postQuotaGroup,
    40  		WriteAccess: rootAccess{},
    41  		ReadAccess:  openAccess{},
    42  	}
    43  	quotaGroupInfoCmd = &Command{
    44  		Path:       "/v2/quotas/{group}",
    45  		GET:        getQuotaGroupInfo,
    46  		ReadAccess: openAccess{},
    47  	}
    48  )
    49  
    50  type postQuotaGroupData struct {
    51  	// Action can be "ensure" or "remove"
    52  	Action    string   `json:"action"`
    53  	GroupName string   `json:"group-name"`
    54  	MaxMemory uint64   `json:"max-memory,omitempty"`
    55  	Parent    string   `json:"parent,omitempty"`
    56  	Snaps     []string `json:"snaps,omitempty"`
    57  }
    58  
    59  var (
    60  	servicestateCreateQuota = servicestate.CreateQuota
    61  	servicestateUpdateQuota = servicestate.UpdateQuota
    62  	servicestateRemoveQuota = servicestate.RemoveQuota
    63  )
    64  
    65  var getQuotaMemUsage = func(grp *quota.Group) (quantity.Size, error) {
    66  	return grp.CurrentMemoryUsage()
    67  }
    68  
    69  // getQuotaGroups returns all quota groups sorted by name.
    70  func getQuotaGroups(c *Command, r *http.Request, _ *auth.UserState) Response {
    71  	st := c.d.overlord.State()
    72  	st.Lock()
    73  	defer st.Unlock()
    74  
    75  	quotas, err := servicestate.AllQuotas(st)
    76  	if err != nil {
    77  		return InternalError(err.Error())
    78  	}
    79  
    80  	i := 0
    81  	names := make([]string, len(quotas))
    82  	for name := range quotas {
    83  		names[i] = name
    84  		i++
    85  	}
    86  	sort.Strings(names)
    87  
    88  	results := make([]client.QuotaGroupResult, len(quotas))
    89  	for i, name := range names {
    90  		qt := quotas[name]
    91  
    92  		memoryUsage, err := getQuotaMemUsage(qt)
    93  		if err != nil {
    94  			return InternalError(err.Error())
    95  		}
    96  
    97  		results[i] = client.QuotaGroupResult{
    98  			GroupName:     qt.Name,
    99  			Parent:        qt.ParentGroup,
   100  			Subgroups:     qt.SubGroups,
   101  			Snaps:         qt.Snaps,
   102  			MaxMemory:     uint64(qt.MemoryLimit),
   103  			CurrentMemory: uint64(memoryUsage),
   104  		}
   105  	}
   106  	return SyncResponse(results)
   107  }
   108  
   109  // getQuotaGroupInfo returns details of a single quota Group.
   110  func getQuotaGroupInfo(c *Command, r *http.Request, _ *auth.UserState) Response {
   111  	vars := muxVars(r)
   112  	groupName := vars["group"]
   113  	if err := naming.ValidateQuotaGroup(groupName); err != nil {
   114  		return BadRequest(err.Error())
   115  	}
   116  
   117  	st := c.d.overlord.State()
   118  	st.Lock()
   119  	defer st.Unlock()
   120  
   121  	group, err := servicestate.GetQuota(st, groupName)
   122  	if err == servicestate.ErrQuotaNotFound {
   123  		return NotFound("cannot find quota group %q", groupName)
   124  	}
   125  	if err != nil {
   126  		return InternalError(err.Error())
   127  	}
   128  
   129  	memoryUsage, err := getQuotaMemUsage(group)
   130  	if err != nil {
   131  		return InternalError(err.Error())
   132  	}
   133  
   134  	res := client.QuotaGroupResult{
   135  		GroupName:     group.Name,
   136  		Parent:        group.ParentGroup,
   137  		Snaps:         group.Snaps,
   138  		Subgroups:     group.SubGroups,
   139  		MaxMemory:     uint64(group.MemoryLimit),
   140  		CurrentMemory: uint64(memoryUsage),
   141  	}
   142  	return SyncResponse(res)
   143  }
   144  
   145  // postQuotaGroup creates quota resource group or updates an existing group.
   146  func postQuotaGroup(c *Command, r *http.Request, _ *auth.UserState) Response {
   147  	var data postQuotaGroupData
   148  
   149  	decoder := json.NewDecoder(r.Body)
   150  	if err := decoder.Decode(&data); err != nil {
   151  		return BadRequest("cannot decode quota action from request body: %v", err)
   152  	}
   153  
   154  	if err := naming.ValidateQuotaGroup(data.GroupName); err != nil {
   155  		return BadRequest(err.Error())
   156  	}
   157  
   158  	st := c.d.overlord.State()
   159  	st.Lock()
   160  	defer st.Unlock()
   161  
   162  	switch data.Action {
   163  	case "ensure":
   164  		// check if the quota group exists first, if it does then we need to
   165  		// update it instead of create it
   166  		_, err := servicestate.GetQuota(st, data.GroupName)
   167  		if err != nil && err != servicestate.ErrQuotaNotFound {
   168  			return InternalError(err.Error())
   169  		}
   170  		if err == servicestate.ErrQuotaNotFound {
   171  			// then we need to create the quota
   172  			if err := servicestateCreateQuota(st, data.GroupName, data.Parent, data.Snaps, quantity.Size(data.MaxMemory)); err != nil {
   173  				// XXX: dedicated error type?
   174  				return BadRequest(err.Error())
   175  			}
   176  		} else if err == nil {
   177  			// the quota group already exists, update it
   178  			updateOpts := servicestate.QuotaGroupUpdate{
   179  				AddSnaps:       data.Snaps,
   180  				NewMemoryLimit: quantity.Size(data.MaxMemory),
   181  			}
   182  			if err := servicestateUpdateQuota(st, data.GroupName, updateOpts); err != nil {
   183  				return BadRequest(err.Error())
   184  			}
   185  		}
   186  
   187  	case "remove":
   188  		if err := servicestateRemoveQuota(st, data.GroupName); err != nil {
   189  			return BadRequest(err.Error())
   190  		}
   191  	default:
   192  		return BadRequest("unknown quota action %q", data.Action)
   193  	}
   194  	return SyncResponse(nil)
   195  }