Add instance types #6564

Merged
merged 1 commit into from Nov 24, 2016

Conversation

Projects
None yet
5 participants
Contributor

perrito666 commented Nov 14, 2016

Added InstanceTypes API endpoint that allows to learn
the available instance types in the model region/zone
to make better deployment decisions.

Tests are missing for 3 out of 4 providers implemented but I am keen to get the rest of the code reviewed as this needs to be merged soon.

This follows: https://docs.google.com/document/d/1m6iNWOMYyGwHbDbl8-u_VSZozeZQklxUOgM5t_0wbMQ/edit#heading=h.kc1rayquamxb

###QA
Run Tests.
Create an API client call that points to the controller and Perform the call for the different providers to obtain lists of available types. (This is intended to be used by the GUI)

Contributor

perrito666 commented Nov 15, 2016

!!build!!

Contributor

perrito666 commented Nov 15, 2016

!!try!!

Contributor

perrito666 commented Nov 15, 2016

!!try!!

Mostly looking good. I think we should get input from @urosj or someone on GUI about cost/currency details.

apiserver/provider/backend.go
+package provider
+
+import (
+ names "gopkg.in/juju/names.v2"
@axw

axw Nov 16, 2016

Member

drop alias (goimports is doing this to you too?)

@perrito666

perrito666 Nov 16, 2016

Contributor

yes, I just realized that, mm, Ill have to check what is going on with goimports so I can disable it

apiserver/provider/provider.go
+
+// NewAPI returns an API pointer.
+func NewAPI(backend Backend, authorizer facade.Authorizer, envNew environs.NewEnvironFunc) (*API, error) {
+ if !authorizer.AuthModelManager() {
@axw

axw Nov 16, 2016

Member

The instance types API is intended for users to call.

apiserver/provider/provider.go
+ }, nil
+}
+
+type modelConfigAble interface {
@axw

axw Nov 16, 2016

Member

use environs.ConfigGetter?

@perrito666

perrito666 Nov 16, 2016

Contributor

Does not satisfy what we need here (lacks err)

apiserver/provider/provider.go
+ return params.InstanceTypesResults{}, errors.Trace(err)
+ }
+
+ for i, c := range cons.Constraints {
@axw

axw Nov 16, 2016

Member

Please add a TODO to cache the results. We need to avoid repeatedly querying the cloud, but cannot poll the cloud for reasons described in the spec.

apiserver/provider/provider_test.go
+
+var _ = gc.Suite(&providerTypesSuite{})
+
+var over9kCPUCores uint64 = 9001
apiserver/provider/provider_test.go
+ m := mockEnviron{
+ results: map[constraints.Value]instances.InstanceTypesWithCostMetadata{
+ itCons: instances.InstanceTypesWithCostMetadata{
+ CostUnit: "USD/h",
@axw

axw Nov 16, 2016

Member

It looks weird having both USD/h and USD, but let's come back to it after seeing exactly how the GUI wants to format things. Easy enough to massage into shape.

environs/instances/instancetype.go
+// InstanceTypesWithCostMetadata holds an array of InstanceType and metadata
+// about their cost.
+type InstanceTypesWithCostMetadata struct {
+ InstanceTypes []InstanceType
@axw

axw Nov 16, 2016

Member

Field comments please.

@@ -0,0 +1,33 @@
+package azure
@axw

axw Nov 16, 2016

Member

copyright

+package azure
+
+import (
+ "github.com/juju/errors"
+ }
+ result := make([]instances.InstanceType, len(types))
+ i := 0
+ for _, iType := range types {
@axw

axw Nov 16, 2016

Member
for i, iType := range types {
    result[i] = iType
}
@perrito666

perrito666 Nov 16, 2016

Contributor

types is a map, i would be the key, wouldnt it?

@axw

axw Nov 16, 2016

Member

Derp, so it is.

@rogpeppe

rogpeppe Nov 16, 2016

Owner

My preferred idiom for that is:

result := make([]instances.InstanceType, 0, len(types))
for _, iType := range types {
    result = append(result, iType)
}
@perrito666

perrito666 Nov 16, 2016

Contributor

@rogpeppe cool, I guess I know why but could you expand on the underlying of that?

@axw

axw Nov 20, 2016

Member

@perrito666 like rogpeppe, I do the same when going from map to slice. Only because it's more compact and reads better (to me).

+ result[i] = iType
+ i++
+ }
+ result, err = instances.MatchingInstanceTypes(result, "", c)
@axw

axw Nov 16, 2016

Member

I wasn't suggesting you move this into each provider. You can/should do this outside of each provider, but you also need the constraints in the provider-level API because some providers will need to generate results based on those constraints. i.e. the constraints are required for generic filtering, but also for generating custom instance types.

@perrito666

perrito666 Nov 16, 2016

Contributor

Well, do we really want double filtering? I believe that its saner if each provider filters its own types but am open to be convinced of the contrary.

@axw

axw Nov 16, 2016

Member

I'm not too fussed, I'd just prefer not to pass the buck to the provider if we can avoid it, and also avoid repeating code in each provider. Maybe it's better to have the responsibility of the provider clear, and require that it honour the constraints.

@@ -0,0 +1,16 @@
+package cloudsigma
@axw

axw Nov 16, 2016

Member

copyright

+package cloudsigma
+
+import (
+ "github.com/juju/errors"
@@ -0,0 +1,16 @@
+package dummy
@axw

axw Nov 16, 2016

Member

copyright
(etc.)

+package dummy
+
+import (
+ "github.com/juju/errors"
@axw

axw Nov 16, 2016

Member

\n
(etc.)

provider/ec2/instance_information.go
+ return instances.InstanceTypesWithCostMetadata{}, errors.Trace(err)
+ }
+ for i, t := range iTypes {
+ iTypes[i].Cost = t.Cost / 1000
@axw

axw Nov 16, 2016

Member

Nope. I must have been unclear, sorry.

What I meant was to include a divisor in the result struct, not to truncate/drop precision in the cost field. i.e. the results should include:

  • the cost (e.g. in deci-cents)
  • a divisor (e.g. of 1000) to get to dollars, or whatever the unit is
  • the currency/unit (e.g. $USD/h)

e.g. with cost=1234, divisor=1000, currency/unit=$USD/h, the GUI could display "1.234 $USD/h".

@perrito666

perrito666 Nov 16, 2016

Contributor

gotcha, will do

+ }
+
+ result := make([]instances.InstanceType, len(resultUnique))
+ i := 0
@axw

axw Nov 16, 2016

Member

for i, it := range resultUnique {

@perrito666

perrito666 Nov 16, 2016

Contributor

once again, map. :p

axw approved these changes Nov 16, 2016

LGTM, but let's get feedback from @urosj before committing to this.

apiserver/params/instance_information.go
+ InstanceTypes []InstanceType `json:"instance-types,omitempty"`
+ CostUnit string `json:"cost-unit,omitempty"`
+ CostCurrency string `json:"cost-currency,omitempty"`
+ Divisor uint64 `json:"divisor,omitempty"`
@axw

axw Nov 16, 2016

Member

CostDivisor/cost-divisor please

environs/instances/instancetype.go
+ CostCurrency string
+ // Divisor indicates a number that must be applied to InstanceType.Cost to obtain
+ // a number that is in CostUnit.
+ Divisor uint64
@axw

axw Nov 16, 2016

Member

CostDivisor

@axw

axw Nov 16, 2016

Member

I think we should document that setting (Cost)Divisor to zero will mean that the cost is treated as already being in specified unit, as the (cost-)divisor field will be omitted from the API response. i.e. the zero value is equivalent to 1.

@@ -0,0 +1,60 @@
+package gce
@axw

axw Nov 16, 2016

Member

copyright

+import (
+ "strconv"
+
+ "github.com/juju/errors"
provider/gce/instance_information.go
+ "github.com/juju/juju/constraints"
+ "github.com/juju/juju/environs"
+ "github.com/juju/juju/environs/instances"
+ "github.com/juju/utils/arch"
@axw

axw Nov 16, 2016

Member

move me

@@ -0,0 +1,23 @@
+package joyent
@axw

axw Nov 16, 2016

Member

copyright

@@ -0,0 +1,15 @@
+package lxd
@axw

axw Nov 16, 2016

Member

copyright

+package lxd
+
+import (
+ "github.com/juju/errors"
@@ -0,0 +1,15 @@
+package maas
@axw

axw Nov 16, 2016

Member

copyright

+package maas
+
+import (
+ "github.com/juju/errors"
@@ -0,0 +1,16 @@
+package manual
@axw

axw Nov 16, 2016

Member

copyright

+package manual
+
+import (
+ "github.com/juju/errors"
@@ -0,0 +1,15 @@
+package openstack
@axw

axw Nov 16, 2016

Member

copyright

+package openstack
+
+import (
+ "github.com/juju/errors"
@@ -0,0 +1,15 @@
+package rackspace
@axw

axw Nov 16, 2016

Member

copyright

+package rackspace
+
+import (
+ "github.com/juju/errors"
+
+var _ environs.InstanceTypesFetcher = (*environ)(nil)
+
+func (e environ) InstanceTypes(c constraints.Value) (instances.InstanceTypesWithCostMetadata, error) {
@axw

axw Nov 16, 2016

Member

do we need this one? I think the openstack impl is sufficient?

@@ -0,0 +1,16 @@
+package vsphere
@axw

axw Nov 16, 2016

Member

copyright

+package vsphere
+
+import (
+ "github.com/juju/errors"
apiserver/provider/provider.go
+ costUnit := instanceTypes.CostUnit
+ costCurrency := instanceTypes.CostCurrency
+ divisor := instanceTypes.CostDivisor
+ if err != nil {
@rogpeppe

rogpeppe Nov 16, 2016

Owner

Move this next to the call to InstanceTypes ?

apiserver/provider/provider.go
+
+ res[i] = params.InstanceTypesResult{
+ InstanceTypes: toParamsInstanceTypeResult(allTypes),
+ CostUnit: costUnit,
@rogpeppe

rogpeppe Nov 16, 2016

Owner

Inline? i.e. use instanceTypes.CostUnit instead of using a temp variable.

+
+// InstanceTypesFetcher is an interface that allows for instance information from
+// a provider to be obtained.
+type InstanceTypesFetcher interface {
@rogpeppe

rogpeppe Nov 16, 2016

Owner

Is it really worth having a separate interface type for this single method? Why not just make it part of Environ?

@perrito666

perrito666 Nov 16, 2016

Contributor

Separation of concerns

What's there LGTM, but the apiserver facade changes are missing.

+
+// ModelInstanceTypesConstraint contains a constraint applied when filtering instance types.
+type ModelInstanceTypesConstraint struct {
+ // Value, if specified, contains the constraints to filter
@axw

axw Nov 20, 2016

Member

Can you please change the name to Constraints, and tag to "constraints", to match the field in CloudInstanceTypesConstraints? That way one is a superset of the other.

@axw

axw Nov 23, 2016

Member

Ping. This one is not so small, please don't forget about this

@perrito666

perrito666 Nov 23, 2016

Contributor

Fixed

+ InstanceTypes []InstanceType `json:"instance-types,omitempty"`
+ CostUnit string `json:"cost-unit,omitempty"`
+ CostCurrency string `json:"cost-currency,omitempty"`
+ // CostDivisor Will be present only when the Cost is not expressed in CostUnit.
@axw

axw Nov 20, 2016

Member

s/Will/will/

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

+ }
+ result := make([]instances.InstanceType, len(types))
+ i := 0
+ for _, iType := range types {
@axw

axw Nov 16, 2016

Member
for i, iType := range types {
    result[i] = iType
}
@perrito666

perrito666 Nov 16, 2016

Contributor

types is a map, i would be the key, wouldnt it?

@axw

axw Nov 16, 2016

Member

Derp, so it is.

@rogpeppe

rogpeppe Nov 16, 2016

Owner

My preferred idiom for that is:

result := make([]instances.InstanceType, 0, len(types))
for _, iType := range types {
    result = append(result, iType)
}
@perrito666

perrito666 Nov 16, 2016

Contributor

@rogpeppe cool, I guess I know why but could you expand on the underlying of that?

@axw

axw Nov 20, 2016

Member

@perrito666 like rogpeppe, I do the same when going from map to slice. Only because it's more compact and reads better (to me).

+ },
+ }
+ return &machineType, nil
+
@axw

axw Nov 20, 2016

Member

delete empty line

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

+)
+
+var _ environs.InstanceTypesFetcher = (*environ)(nil)
+var virtType = "kvm"
@axw

axw Nov 20, 2016

Member

const?

@perrito666

perrito666 Nov 23, 2016

Contributor

if I use a const I cannot pass its addres afterwards (instances.InstanceType.VirtType is a pointer)

+package cloud
+
+import (
+ "github.com/juju/errors"
@axw

axw Nov 20, 2016

Member

move me

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

apiserver/cloud/instance_information.go
+ "github.com/juju/juju/environs"
+ "github.com/juju/juju/state"
+ "github.com/juju/juju/state/stateenvirons"
+ names "gopkg.in/juju/names.v2"
@axw

axw Nov 20, 2016

Member

move me, drop alias

apiserver/cloud/instance_information.go
+// EnvironConfigGetter implements environs.EnvironConfigGetter
+// in terms of a *state.State.
+type cloudEnvironConfigGetter struct {
+ *state.State
@axw

axw Nov 20, 2016

Member

You only need state.CloudAccessor and the GetModel function. Backend (apiserver/cloud/backend.go) already embeds state.CloudAccessor, so you just need to add the GetModel method, and you can use Backend instead of State.

You could otherwise just ignore the ModelTag, and use the existing Backend.ControllerModel method.

I think it might be simpler though if you just pass the CloudSpec (or perhaps Environ?) into the common.InstanceTypes function? i.e. have the facades pull apart the args, get the appropriate model, cloud, and region, and construct a CloudSpec from them. You may as well then get the Environ then too, and avoid passing the backend in just to get ModelConfig?

+
+// InstanceTypes returns instance type information for the cloud and region
+// in which the current model is deployed.
+func (api *CloudAPI) InstanceTypes(cons params.CloudInstanceTypesConstraints) (params.InstanceTypesResults, error) {
@axw

axw Nov 20, 2016

Member

we need a test or two for this

@axw

axw Nov 23, 2016

Member

Fixed

+ if cons.Constraints != nil {
+ value = *cons.Constraints
+ }
+ backend := cloudEnvironConfigGetter{
@axw

axw Nov 20, 2016

Member

Please parse the cons.CloudTag here, and check that it is the same as the controller model's cloud. We may relax this later, but it's better to be strict to avoid having to deal with sloppily written clients later.

@axw

axw Nov 23, 2016

Member

Fixed

apiserver/cloud/instance_information.go
+ itCons := common.NewInstanceTypeConstraints(backend, value, modelTag)
+ it, err := common.InstanceTypes(itCons)
+ if err != nil {
+ return params.InstanceTypesResults{}, errors.Trace(err)
@axw

axw Nov 20, 2016

Member

An error due to one of them failing should not cause all of them to fail.

Either assume that common.InstanceTypes never fills in the Error field, and if it returns an error, have the caller (Cloud or MachineManager facade) fill it in; or change common.InstanceTypes to not have an error result.

apiserver/common/instancetypes.go
+ environs.EnvironConfigGetter
+ ModelTag() names.ModelTag
+ Close() error
+ GetModel(names.ModelTag) (*state.Model, error)
@axw

axw Nov 20, 2016

Member

please not state.Model, use an interface

@perrito666

perrito666 Nov 21, 2016

Contributor

This is mirroring state, there is nothing I can do.

@axw

axw Nov 21, 2016

Member

Don't be so defeatist ;)

Look at ControllerModel in apiserver/cloud/backend.go for an example of how to do this. Anyway, I think this will be moot if you pass the Environ in?

@perrito666

perrito666 Nov 21, 2016

Contributor

I realized that seconds after writing that and fixed it with ashim, and am now writing the tests

apiserver/common/instancetypes.go
+// InstanceTypes returns a list of the available instance types in the provider according
+// to the passed constraints.
+func InstanceTypes(cons instanceTypeConstraints) (params.InstanceTypesResult, error) {
+ m, err := cons.backend.GetModel(cons.modelTag)
@axw

axw Nov 20, 2016

Member

The fact that we're calling GetModel multiple times for what's always going to be the same model is a bit of a smell I think. As mentioned before, I think we should pass the CloudSpec (perhaps Environ?) in.

If this were split out, then it would be reasonable to fail the entire API call if we fail to get the CloudSpec in the case of the MachineManager facade. For the Cloud facade, it would be reasonable to fail the entire API call if we fail to get the Model; and then fail individual results if we can't get the CloudSpec (e.g. if an invalid CloudTag or region is specified).

Looking better, thanks.

apiserver/common/instancetypes.go
+// information.
+type InstanceTypeBackend interface {
+ environs.EnvironConfigGetter
+ Close() error
@axw

axw Nov 22, 2016

Member

Unused, drop me. I don't think it's appropriate to hand off ownership/responsibility to InstanceTypes anyway.

apiserver/common/instancetypes.go
+ Config() (*config.Config, error)
+}
+
+// NewModelEnvironConfigGetter can create
@axw

axw Nov 22, 2016

Member

Can you please move this, modelEnvironConfigGetter, and InstanceTypeBackend to a separate file? It's only tangentially related to instance types. Also, InstanceTypeBackend should be renamed to be something to do with model/environ config.

It might be simpler just to have this as a struct of two functions? e.g.

type EnvironConfigGetterFuncs struct {
    ModelConfigFunc func() (*config.Config, error)
    CloudSpecFunc func(names.ModelTag) (environs.CloudSpec, error)
}

func (f EnvironConfigGetterFuncs) ModelConfig() (*config.Config, error) {
    return f.ModelConfigFunc()
}

...

Just a thought, feel free to ignore that.

The EnvironConfigGetter interface is kinda weird. Both methods should probably be taking a ModelTag...

+
+// InstanceTypes returns a list of the available instance types in the provider according
+// to the passed constraints.
+func InstanceTypes(cons instanceTypeConstraints) (params.InstanceTypesResult, error) {
@axw

axw Nov 22, 2016

Member

Why pass around an unexported type?

@perrito666

perrito666 Nov 23, 2016

Contributor

Fixed

+// InstanceTypes returns instance type information for the cloud and region
+// in which the current model is deployed.
+func (mm *MachineManagerAPI) InstanceTypes(cons params.ModelInstanceTypesConstraints) (params.InstanceTypesResults, error) {
+ st := mm.st.(*state.State)
@axw

axw Nov 22, 2016

Member

Please do the same for this method as you did for the other one, and use interfaces (stateInterface).

jujutesting.Stub
results map[constraints.Value]instances.InstanceTypesWithCostMetadata
}
func (m *mockEnviron) InstanceTypes(c constraints.Value) (instances.InstanceTypesWithCostMetadata, error) {
+ fmt.Println(c)
apiserver/machinemanager/state.go
+ return m, nil
+}
+
+func (s stateShim) Cloud(cloudName string) (cloud.Cloud, error) {
@axw

axw Nov 22, 2016

Member

these Cloud* methods aren't needed? stateShim embeds State, and State already has those methods

axw approved these changes Nov 23, 2016

LGTM once remaining comments are addressed (those from this review and ones from before, unless we've already talked about them.)

+ "github.com/juju/juju/state/stateenvirons"
+)
+
+// EnvironConfigGetter implements environs.EnvironConfigGetter
@axw

axw Nov 23, 2016

Member

cloudEnvironConfigGetter

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

+ return params.InstanceTypesResults{}, errors.Trace(err)
+ }
+ modelTag := c.ModelTag()
+ m, err := api.backend.GetModel(modelTag)
@axw

axw Nov 23, 2016

Member

you end up with the same model as returned by ControllerModel

just do m, err := api.backend.ControllerModel()

then you can get rid of the ModelTag method from Backend

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

+ "github.com/juju/juju/environs/config"
+)
+
+// EnvironConfigGetterFuncs holds implements environs.EnvironConfigGetter
@axw

axw Nov 23, 2016

Member

s/holds //

@perrito666

perrito666 Nov 23, 2016

Contributor

fixed

+
+// ModelInstanceTypesConstraint contains a constraint applied when filtering instance types.
+type ModelInstanceTypesConstraint struct {
+ // Value, if specified, contains the constraints to filter
@axw

axw Nov 20, 2016

Member

Can you please change the name to Constraints, and tag to "constraints", to match the field in CloudInstanceTypesConstraints? That way one is a superset of the other.

@axw

axw Nov 23, 2016

Member

Ping. This one is not so small, please don't forget about this

@perrito666

perrito666 Nov 23, 2016

Contributor

Fixed

Looks good and works well. Just a couple of minor issues. Did you talk about opening the Cloud call up to anonymous users?

+ if m.Cloud() != cloudTag.Id() {
+ result[i] = params.InstanceTypesResult{Error: common.ServerError(errors.NotValidf("asking %s cloud information to %s cloud", cloudTag.Id(), m.Cloud()))}
+ continue
+ }
@frankban

frankban Nov 23, 2016

Member

I'd check also that cons.CloudRegion is not empty here. otherwise I guess the subsequent error would be ugly and not really informative. This case should also be tested.

+ ) (environs.Environ, error) {
+ return &env, nil
+ }
+ r, err := machinemanager.InstanceTypes(&api, fakeEnvironGet, cons)
@frankban

frankban Nov 23, 2016

Member

Should we also test the case no constraints are passed? In this case I'd expect an empty result.

apiserver/cloud/instance_information.go
@@ -75,6 +75,10 @@ func instanceTypes(api *CloudAPI,
result[i] = params.InstanceTypesResult{Error: common.ServerError(errors.NotValidf("asking %s cloud information to %s cloud", cloudTag.Id(), m.Cloud()))}
continue
}
+ if cons.CloudRegion == "" {
@axw

axw Nov 23, 2016

Member

Not all clouds have regions. I'm not sure if the validation belongs here or not, but if we do validate, we need to check that the cloud supports regions before requiring that it's empty.

Member

axw commented Nov 23, 2016

@frankban

Looks good and works well. Just a couple of minor issues. Did you talk about opening the Cloud call up to anonymous users?

Yes we have talked about it, but as we agreed we'll defer this bit. It's not feasible to make that change for 2.1.

Add InstanceTypes API endpoint.
Added InstanceTypes API endpoint that allows to learn
the available instance types in the model region/zone
to make better deployment decisions.
Contributor

perrito666 commented Nov 24, 2016

!!try!!

Contributor

perrito666 commented Nov 24, 2016

$$merge$$

Contributor

jujubot commented Nov 24, 2016

Status: merge request accepted. Url: http://juju-ci.vapour.ws:8080/job/github-merge-juju

@jujubot jujubot merged commit acfb0ce into juju:develop Nov 24, 2016

1 check passed

github-check-merge-juju Built PR, ran unit tests, and tested LXD deploy. Use !!.*!! to request another build. IE, !!build!!, !!retry!!
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment