github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/overlord/servicestate/quota_control.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 servicestate 21 22 import ( 23 "fmt" 24 25 "github.com/snapcore/snapd/features" 26 "github.com/snapcore/snapd/gadget/quantity" 27 "github.com/snapcore/snapd/logger" 28 "github.com/snapcore/snapd/osutil" 29 "github.com/snapcore/snapd/overlord/configstate/config" 30 "github.com/snapcore/snapd/overlord/state" 31 "github.com/snapcore/snapd/snapdenv" 32 "github.com/snapcore/snapd/systemd" 33 ) 34 35 var ( 36 systemdVersion int 37 ) 38 39 // TODO: move to a systemd.AtLeast() ? 40 func checkSystemdVersion() error { 41 vers, err := systemd.Version() 42 if err != nil { 43 return err 44 } 45 systemdVersion = vers 46 return nil 47 } 48 49 func init() { 50 if err := checkSystemdVersion(); err != nil { 51 logger.Noticef("failed to check systemd version: %v", err) 52 } 53 } 54 55 // MockSystemdVersion mocks the systemd version to the given version. This is 56 // only available for unit tests and will panic when run in production. 57 func MockSystemdVersion(vers int) (restore func()) { 58 osutil.MustBeTestBinary("cannot mock systemd version outside of tests") 59 old := systemdVersion 60 systemdVersion = vers 61 return func() { 62 systemdVersion = old 63 } 64 } 65 66 func quotaGroupsAvailable(st *state.State) error { 67 // check if the systemd version is too old 68 if systemdVersion < 205 { 69 return fmt.Errorf("systemd version too old: snap quotas requires systemd 205 and newer (currently have %d)", systemdVersion) 70 } 71 72 tr := config.NewTransaction(st) 73 enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups) 74 if err != nil && !config.IsNoOption(err) { 75 return err 76 } 77 if !enableQuotaGroups { 78 return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true") 79 } 80 81 return nil 82 } 83 84 // CreateQuota attempts to create the specified quota group with the specified 85 // snaps in it. 86 // TODO: should this use something like QuotaGroupUpdate with fewer fields? 87 func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error { 88 if err := quotaGroupsAvailable(st); err != nil { 89 return err 90 } 91 92 allGrps, err := AllQuotas(st) 93 if err != nil { 94 return err 95 } 96 97 // TODO: switch to returning a taskset with the right handler instead of 98 // executing this directly 99 qc := QuotaControlAction{ 100 Action: "create", 101 QuotaName: name, 102 MemoryLimit: memoryLimit, 103 AddSnaps: snaps, 104 ParentName: parentName, 105 } 106 107 return quotaCreate(st, nil, qc, allGrps, nil, nil) 108 } 109 110 // RemoveQuota deletes the specific quota group. Any snaps currently in the 111 // quota will no longer be in any quota group, even if the quota group being 112 // removed is a sub-group. 113 // TODO: currently this only supports removing leaf sub-group groups, it doesn't 114 // support removing parent quotas, but probably it makes sense to allow that too 115 func RemoveQuota(st *state.State, name string) error { 116 if snapdenv.Preseeding() { 117 return fmt.Errorf("removing quota groups not supported while preseeding") 118 } 119 120 allGrps, err := AllQuotas(st) 121 if err != nil { 122 return err 123 } 124 125 // TODO: switch to returning a taskset with the right handler instead of 126 // executing this directly 127 qc := QuotaControlAction{ 128 Action: "remove", 129 QuotaName: name, 130 } 131 132 return quotaRemove(st, nil, qc, allGrps, nil, nil) 133 } 134 135 // QuotaGroupUpdate reflects all of the modifications that can be performed on 136 // a quota group in one operation. 137 type QuotaGroupUpdate struct { 138 // AddSnaps is the set of snaps to add to the quota group. These are 139 // instance names of snaps, and are appended to the existing snaps in 140 // the quota group 141 AddSnaps []string 142 143 // NewMemoryLimit is the new memory limit to be used for the quota group. If 144 // zero, then the quota group's memory limit is not changed. 145 NewMemoryLimit quantity.Size 146 } 147 148 // UpdateQuota updates the quota as per the options. 149 // TODO: this should support more kinds of updates such as moving groups between 150 // parents, removing sub-groups from their parents, and removing snaps from 151 // the group. 152 func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) error { 153 if err := quotaGroupsAvailable(st); err != nil { 154 return err 155 } 156 157 allGrps, err := AllQuotas(st) 158 if err != nil { 159 return err 160 } 161 162 // TODO: switch to returning a taskset with the right handler instead of 163 // executing this directly 164 qc := QuotaControlAction{ 165 Action: "update", 166 QuotaName: name, 167 MemoryLimit: updateOpts.NewMemoryLimit, 168 AddSnaps: updateOpts.AddSnaps, 169 } 170 171 return quotaUpdate(st, nil, qc, allGrps, nil, nil) 172 } 173 174 // EnsureSnapAbsentFromQuota ensures that the specified snap is not present 175 // in any quota group, usually in preparation for removing that snap from the 176 // system to keep the quota group itself consistent. 177 // This function is idempotent, since if it was interrupted after unlocking the 178 // state inside ensureSnapServicesForGroup it will not re-execute since the 179 // specified snap will not be present inside the group reference in the state. 180 func EnsureSnapAbsentFromQuota(st *state.State, snap string) error { 181 allGrps, err := AllQuotas(st) 182 if err != nil { 183 return err 184 } 185 186 // try to find the snap in any group 187 for _, grp := range allGrps { 188 for idx, sn := range grp.Snaps { 189 if sn == snap { 190 // drop this snap from the list of Snaps by swapping it with the 191 // last snap in the list, and then dropping the last snap from 192 // the list 193 grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1] 194 grp.Snaps = grp.Snaps[:len(grp.Snaps)-1] 195 196 // update the quota group state 197 allGrps, err = patchQuotas(st, grp) 198 if err != nil { 199 return err 200 } 201 202 // ensure service states are updated - note we have to add the 203 // snap as an extra snap to ensure since it was removed from the 204 // group and thus won't be considered just by looking at the 205 // group pointer directly 206 opts := &ensureSnapServicesForGroupOptions{ 207 allGrps: allGrps, 208 extraSnaps: []string{snap}, 209 } 210 // TODO: we could pass timing and progress here from the task we 211 // are executing as eventually 212 return ensureSnapServicesForGroup(st, nil, grp, opts, nil, nil) 213 } 214 } 215 } 216 217 // the snap wasn't in any group, nothing to do 218 return nil 219 }