Skip to content

Commit 919ef06

Browse files
committed
Add check on account & domain limits before deploying a VM
1 parent 5803cf3 commit 919ef06

File tree

3 files changed

+145
-24
lines changed

3 files changed

+145
-24
lines changed

pkg/cloud/client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type client struct {
5959
cs *cloudstack.CloudStackClient
6060
csAsync *cloudstack.CloudStackClient
6161
config Config
62+
user *User
6263
customMetrics metrics.ACSCustomMetrics
6364
}
6465

@@ -171,6 +172,28 @@ func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap) (Client, err
171172
c.cs = cloudstack.NewAsyncClient(conf.APIUrl, conf.APIKey, conf.SecretKey, verifySSL)
172173
c.csAsync = cloudstack.NewClient(conf.APIUrl, conf.APIKey, conf.SecretKey, verifySSL)
173174
c.customMetrics = metrics.NewCustomMetrics()
175+
176+
p := c.cs.User.NewListUsersParams()
177+
userResponse, err := c.cs.User.ListUsers(p)
178+
if err != nil {
179+
return c, err
180+
}
181+
user := &User{
182+
ID: userResponse.Users[0].Id,
183+
Account: Account{
184+
Name: userResponse.Users[0].Account,
185+
Domain: Domain{
186+
Path: userResponse.Users[0].Domain,
187+
},
188+
},
189+
}
190+
if found, err := c.GetUserWithKeys(user); err != nil {
191+
return nil, err
192+
} else if !found {
193+
return nil, errors.Errorf(
194+
"could not find sufficient user (with API keys) in domain/account %s/%s", userResponse.Users[0].Domain, userResponse.Users[0].Account)
195+
}
196+
c.user = user
174197
clientCache.Set(clientCacheKey, c)
175198

176199
return c, nil
@@ -189,6 +212,7 @@ func (c *client) NewClientInDomainAndAccount(domain string, account string) (Cli
189212
}
190213
c.config.APIKey = user.APIKey
191214
c.config.SecretKey = user.SecretKey
215+
c.user = user
192216

193217
return NewClientFromConf(c.config, nil)
194218
}

pkg/cloud/instance.go

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cloud
1919
import (
2020
"encoding/base64"
2121
"fmt"
22+
"strconv"
2223
"strings"
2324

2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -85,34 +86,34 @@ func (c *client) ResolveVMInstanceDetails(csMachine *infrav1.CloudStackMachine)
8586
return errors.New("no match found")
8687
}
8788

88-
func (c *client) ResolveServiceOffering(csMachine *infrav1.CloudStackMachine, zoneID string) (offeringID string, retErr error) {
89+
func (c *client) ResolveServiceOffering(csMachine *infrav1.CloudStackMachine, zoneID string) (offering cloudstack.ServiceOffering, retErr error) {
8990
if len(csMachine.Spec.Offering.ID) > 0 {
9091
csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByID(csMachine.Spec.Offering.ID)
9192
if err != nil {
9293
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
93-
return "", multierror.Append(retErr, errors.Wrapf(
94+
return *csOffering, multierror.Append(retErr, errors.Wrapf(
9495
err, "could not get Service Offering by ID %s", csMachine.Spec.Offering.ID))
9596
} else if count != 1 {
96-
return "", multierror.Append(retErr, errors.Errorf(
97+
return *csOffering, multierror.Append(retErr, errors.Errorf(
9798
"expected 1 Service Offering with UUID %s, but got %d", csMachine.Spec.Offering.ID, count))
9899
}
99100

100101
if len(csMachine.Spec.Offering.Name) > 0 && csMachine.Spec.Offering.Name != csOffering.Name {
101-
return "", multierror.Append(retErr, errors.Errorf(
102+
return *csOffering, multierror.Append(retErr, errors.Errorf(
102103
"offering name %s does not match name %s returned using UUID %s", csMachine.Spec.Offering.Name, csOffering.Name, csMachine.Spec.Offering.ID))
103104
}
104-
return csMachine.Spec.Offering.ID, nil
105+
return *csOffering, nil
105106
}
106-
offeringID, count, err := c.cs.ServiceOffering.GetServiceOfferingID(csMachine.Spec.Offering.Name, cloudstack.WithZone(zoneID))
107+
csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByName(csMachine.Spec.Offering.Name, cloudstack.WithZone(zoneID))
107108
if err != nil {
108109
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
109-
return "", multierror.Append(retErr, errors.Wrapf(
110+
return *csOffering, multierror.Append(retErr, errors.Wrapf(
110111
err, "could not get Service Offering ID from %s in zone %s", csMachine.Spec.Offering.Name, zoneID))
111112
} else if count != 1 {
112-
return "", multierror.Append(retErr, errors.Errorf(
113+
return *csOffering, multierror.Append(retErr, errors.Errorf(
113114
"expected 1 Service Offering with name %s in zone %s, but got %d", csMachine.Spec.Offering.Name, zoneID, count))
114115
}
115-
return offeringID, nil
116+
return *csOffering, nil
116117
}
117118

118119
func (c *client) ResolveTemplate(
@@ -206,9 +207,61 @@ func verifyDiskoffering(csMachine *infrav1.CloudStackMachine, c *client, diskOff
206207
return diskOfferingID, nil
207208
}
208209

210+
// CheckAccountLimits Checks the account's limit of VM, CPU & Memory
211+
// before deploying a VM.
212+
func (c *client) CheckAccountLimits(fd *infrav1.CloudStackFailureDomain, offering cloudstack.ServiceOffering) error {
213+
if c.user.Account.CPUAvailable != "Unlimited" {
214+
cpuAvailable, err := strconv.ParseInt(c.user.Account.CPUAvailable, 10, 0)
215+
if err == nil && int64(offering.Cpunumber) > cpuAvailable {
216+
return fmt.Errorf("CPU available (%d) in account can't fulfil the requirement: %d", cpuAvailable, offering.Cpunumber)
217+
}
218+
}
219+
220+
if c.user.Account.MemoryAvailable != "Unlimited" {
221+
memoryAvailable, err := strconv.ParseInt(c.user.Account.MemoryAvailable, 10, 0)
222+
if err == nil && int64(offering.Memory) > memoryAvailable {
223+
return fmt.Errorf("memory available (%d) in account can't fulfil the requirement: %d", memoryAvailable, offering.Memory)
224+
}
225+
}
226+
227+
if c.user.Account.VMAvailable != "Unlimited" {
228+
vmAvailable, err := strconv.ParseInt(c.user.Account.VMAvailable, 10, 0)
229+
if err == nil && vmAvailable <= 0 {
230+
return fmt.Errorf("VM Limit in account has reached it's maximum value")
231+
}
232+
}
233+
return nil
234+
}
235+
236+
// CheckDomainLimits Checks the domain's limit of VM, CPU & Memory
237+
// before deploying a VM.
238+
func (c *client) CheckDomainLimits(fd *infrav1.CloudStackFailureDomain, offering cloudstack.ServiceOffering) error {
239+
if c.user.Account.Domain.CPUAvailable != "Unlimited" {
240+
cpuAvailable, err := strconv.ParseInt(c.user.Account.Domain.CPUAvailable, 10, 0)
241+
if err == nil && int64(offering.Cpunumber) > cpuAvailable {
242+
return fmt.Errorf("CPU available (%d) in domain can't fulfil the requirement: %d", cpuAvailable, offering.Cpunumber)
243+
}
244+
}
245+
246+
if c.user.Account.Domain.MemoryAvailable != "Unlimited" {
247+
memoryAvailable, err := strconv.ParseInt(c.user.Account.Domain.MemoryAvailable, 10, 0)
248+
if err == nil && int64(offering.Memory) > memoryAvailable {
249+
return fmt.Errorf("memory available (%d) in domain can't fulfil the requirement: %d", memoryAvailable, offering.Memory)
250+
}
251+
}
252+
253+
if c.user.Account.Domain.VMAvailable != "Unlimited" {
254+
vmAvailable, err := strconv.ParseInt(c.user.Account.Domain.VMAvailable, 10, 0)
255+
if err == nil && vmAvailable > 0 {
256+
return fmt.Errorf("VM Limit in domain has reached it's maximum value")
257+
}
258+
}
259+
return nil
260+
}
261+
209262
// GetOrCreateVMInstance CreateVMInstance will fetch or create a VM instance, and
210263
// sets the infrastructure machine spec and status accordingly.
211-
func (c *client) GetOrCreateVMInstance(
264+
func (c *client) CheckLimitsAndCreateVM(
212265
csMachine *infrav1.CloudStackMachine,
213266
capiMachine *clusterv1.Machine,
214267
csCluster *infrav1.CloudStackCluster,
@@ -217,27 +270,31 @@ func (c *client) GetOrCreateVMInstance(
217270
userData string,
218271
) error {
219272

220-
// Check if VM instance already exists.
221-
if err := c.ResolveVMInstanceDetails(csMachine); err == nil ||
222-
!strings.Contains(strings.ToLower(err.Error()), "no match") {
273+
offering, err := c.ResolveServiceOffering(csMachine, fd.Spec.Zone.ID)
274+
if err != nil {
223275
return err
224276
}
225277

226-
offeringID, err := c.ResolveServiceOffering(csMachine, fd.Spec.Zone.ID)
278+
templateID, err := c.ResolveTemplate(csCluster, csMachine, fd.Spec.Zone.ID)
227279
if err != nil {
228280
return err
229281
}
230-
templateID, err := c.ResolveTemplate(csCluster, csMachine, fd.Spec.Zone.ID)
282+
diskOfferingID, err := c.ResolveDiskOffering(csMachine, fd.Spec.Zone.ID)
231283
if err != nil {
232284
return err
233285
}
234-
diskOfferingID, err := c.ResolveDiskOffering(csMachine, fd.Spec.Zone.ID)
286+
287+
err = c.CheckAccountLimits(fd, offering)
235288
if err != nil {
236289
return err
237290
}
238291

239-
// Create VM instance.
240-
p := c.cs.VirtualMachine.NewDeployVirtualMachineParams(offeringID, templateID, fd.Spec.Zone.ID)
292+
err = c.CheckDomainLimits(fd, offering)
293+
if err != nil {
294+
return err
295+
}
296+
297+
p := c.cs.VirtualMachine.NewDeployVirtualMachineParams(offering.Id, templateID, fd.Spec.Zone.ID)
241298
p.SetNetworkids([]string{fd.Spec.Zone.Network.ID})
242299
setIfNotEmpty(csMachine.Name, p.SetName)
243300
setIfNotEmpty(capiMachine.Name, p.SetDisplayname)
@@ -289,6 +346,31 @@ func (c *client) GetOrCreateVMInstance(
289346
csMachine.Spec.InstanceID = pointer.String(deployVMResp.Id)
290347
csMachine.Status.Status = pointer.String(metav1.StatusSuccess)
291348
}
349+
return nil
350+
}
351+
352+
// GetOrCreateVMInstance CreateVMInstance will fetch or create a VM instance, and
353+
// sets the infrastructure machine spec and status accordingly.
354+
func (c *client) GetOrCreateVMInstance(
355+
csMachine *infrav1.CloudStackMachine,
356+
capiMachine *clusterv1.Machine,
357+
csCluster *infrav1.CloudStackCluster,
358+
fd *infrav1.CloudStackFailureDomain,
359+
affinity *infrav1.CloudStackAffinityGroup,
360+
userData string,
361+
) error {
362+
363+
// Check if VM instance already exists.
364+
if err := c.ResolveVMInstanceDetails(csMachine); err == nil ||
365+
!strings.Contains(strings.ToLower(err.Error()), "no match") {
366+
return err
367+
}
368+
369+
// Create VM instance.
370+
if err := c.CheckLimitsAndCreateVM(csMachine, capiMachine, csCluster, fd, affinity, userData); err != nil {
371+
return err
372+
}
373+
292374
// Resolve uses a VM metrics request response to fill cloudstack machine status.
293375
// The deployment response is insufficient.
294376
return c.ResolveVMInstanceDetails(csMachine)

pkg/cloud/user_credentials.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,22 @@ type UserCredIFace interface {
3737

3838
// Domain contains specifications that identify a domain.
3939
type Domain struct {
40-
Name string
41-
Path string
42-
ID string
40+
Name string
41+
Path string
42+
ID string
43+
CPUAvailable string
44+
MemoryAvailable string
45+
VMAvailable string
4346
}
4447

4548
// Account contains specifications that identify an account.
4649
type Account struct {
47-
Name string
48-
Domain Domain
49-
ID string
50+
Name string
51+
Domain Domain
52+
ID string
53+
CPUAvailable string
54+
MemoryAvailable string
55+
VMAvailable string
5056
}
5157

5258
// User contains information uniquely identifying and scoping a user.
@@ -105,6 +111,9 @@ func (c *client) ResolveDomain(domain *Domain) error {
105111
}
106112
domain.Path = resp.Domains[0].Path
107113
domain.Name = resp.Domains[0].Name
114+
domain.CPUAvailable = resp.Domains[0].Cpuavailable
115+
domain.MemoryAvailable = resp.Domains[0].Memoryavailable
116+
domain.VMAvailable = resp.Domains[0].Vmavailable
108117
return nil
109118
}
110119

@@ -120,6 +129,9 @@ func (c *client) ResolveDomain(domain *Domain) error {
120129
for _, possibleDomain := range resp.Domains {
121130
if possibleDomain.Path == domain.Path {
122131
domain.ID = possibleDomain.Id
132+
domain.CPUAvailable = possibleDomain.Cpuavailable
133+
domain.MemoryAvailable = possibleDomain.Memoryavailable
134+
domain.VMAvailable = possibleDomain.Vmavailable
123135
return nil
124136
}
125137
}
@@ -150,6 +162,9 @@ func (c *client) ResolveAccount(account *Account) error {
150162
}
151163
account.ID = resp.Accounts[0].Id
152164
account.Name = resp.Accounts[0].Name
165+
account.CPUAvailable = resp.Accounts[0].Cpuavailable
166+
account.MemoryAvailable = resp.Accounts[0].Memoryavailable
167+
account.VMAvailable = resp.Accounts[0].Vmavailable
153168
return nil
154169
}
155170

0 commit comments

Comments
 (0)