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 }