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