From 400940d176c0776439648ea9d386b45efd81f27e Mon Sep 17 00:00:00 2001 From: Nicolas Vinuesa Date: Fri, 29 Sep 2023 13:02:20 +0200 Subject: [PATCH 1/3] Support flexible shapes on Oracle OCI In order to support flexible (https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm#flexible) shapes we must pass some configuration (at least the number of cpu-cores) at the moment of launching the instance. This configuration value will be either the user-passed constraint(s) cpu-cores and memory, else a default minimum cpu-cores. We also define a new constant for minimum cpu-cores that is matched on instances.MatchingInstanceTypes. --- environs/instances/instancetype.go | 34 ++++++++---- environs/instances/instancetype_test.go | 65 ++++++++++++++++------- provider/azure/environ_test.go | 2 +- provider/ec2/image_test.go | 11 ++-- provider/gce/environ_instance_test.go | 2 +- provider/gce/instance_information.go | 11 ++-- provider/gce/instance_information_test.go | 4 +- provider/oci/common_test.go | 27 +++++++++- provider/oci/environ.go | 19 +++++++ provider/oci/environ_test.go | 18 +++++++ provider/oci/images.go | 18 +++++++ provider/oci/images_test.go | 28 +++++++--- provider/oci/provider.go | 21 -------- 13 files changed, 189 insertions(+), 71 deletions(-) diff --git a/environs/instances/instancetype.go b/environs/instances/instancetype.go index a248fe862f7..d2a9857f876 100644 --- a/environs/instances/instancetype.go +++ b/environs/instances/instancetype.go @@ -22,9 +22,11 @@ type InstanceType struct { Cost uint64 RootDisk uint64 // These attributes are not supported by all clouds. - VirtType *string // The type of virtualisation used by the hypervisor, must match the image. - CpuPower *uint64 - Tags []string + VirtType *string // The type of virtualisation used by the hypervisor, must match the image. + CpuPower *uint64 + Tags []string + MaxCpuCores *uint64 + MaxMem *uint64 } // InstanceTypeNetworking hold relevant information about an instances @@ -65,13 +67,17 @@ func (itype InstanceType) match(cons constraints.Value) (InstanceType, bool) { return nothing, false } if cons.CpuCores != nil && itype.CpuCores < *cons.CpuCores { - return nothing, false + if itype.MaxCpuCores == nil || *itype.MaxCpuCores < *cons.CpuCores { + return nothing, false + } } if cons.CpuPower != nil && itype.CpuPower != nil && *itype.CpuPower < *cons.CpuPower { return nothing, false } if cons.Mem != nil && itype.Mem < *cons.Mem { - return nothing, false + if itype.MaxMem == nil || *itype.MaxMem < *cons.Mem { + return nothing, false + } } if cons.RootDisk != nil && itype.RootDisk > 0 && itype.RootDisk < *cons.RootDisk { return nothing, false @@ -85,8 +91,12 @@ func (itype InstanceType) match(cons constraints.Value) (InstanceType, bool) { return itype, true } -// minMemoryHeuristic is the assumed minimum amount of memory (in MB) we prefer in order to run a server (1GB) -const minMemoryHeuristic = 1024 +const ( + // MinCpuCores is the assumed minimum CPU cores we prefer in order to run a server. + MinCpuCores uint64 = 1 + // minMemoryHeuristic is the assumed minimum amount of memory (in MB) we prefer in order to run a server (2GB) + minMemoryHeuristic uint64 = 2048 +) // matchingTypesForConstraint returns instance types from allTypes which match cons. func matchingTypesForConstraint(allTypes []InstanceType, cons constraints.Value) []InstanceType { @@ -108,13 +118,19 @@ func MatchingInstanceTypes(allInstanceTypes []InstanceType, region string, cons // Rules used to select instance types: // - non memory constraints like cores etc are always honoured + // - if no cpu-cores constraint specified, try opinionated default + // with enough cpu cores to run a server. // - if no mem constraint specified and instance-type not specified, // try opinionated default with enough mem to run a server. // - if no matches and no mem constraint specified, try again and // return any matching instance with the largest memory origCons := cons - if !cons.HasInstanceType() && cons.Mem == nil { - minMem := uint64(minMemoryHeuristic) + if !cons.HasInstanceType() && !cons.HasCpuCores() { + minCpuCores := MinCpuCores + cons.CpuCores = &minCpuCores + } + if !cons.HasInstanceType() && !cons.HasMem() { + minMem := minMemoryHeuristic cons.Mem = &minMem } itypes = matchingTypesForConstraint(allInstanceTypes, cons) diff --git a/environs/instances/instancetype_test.go b/environs/instances/instancetype_test.go index 7771e6d5743..6f5c088af4a 100644 --- a/environs/instances/instancetype_test.go +++ b/environs/instances/instancetype_test.go @@ -102,6 +102,15 @@ var instanceTypes = []InstanceType{ Mem: 61952, Cost: 2400, VirtType: &hvm, + }, { + Name: "VM.Standard3.Flex", + Arch: "amd64", + CpuCores: 1, + CpuPower: CpuPower(8800), + MaxCpuCores: makeUint64Pointer(32), + Mem: 6144, + MaxMem: makeUint64Pointer(524288), + Cost: 50, }, } @@ -116,24 +125,31 @@ var getInstanceTypesTest = []struct { about: "cores", cons: "cores=2", expectedItypes: []string{ - "c1.medium", "m1.large", "m1.xlarge", "c1.large", "c1.xlarge", "cc1.4xlarge", - "cc2.8xlarge", + "VM.Standard3.Flex", "m1.large", "m1.xlarge", "c1.large", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", }, }, { - about: "cpu-power", - cons: "cpu-power=2000", - expectedItypes: []string{"c1.xlarge", "cc1.4xlarge", "cc2.8xlarge"}, + about: "17 cores only the flexible shape", + cons: "cores=17", + expectedItypes: []string{ + "VM.Standard3.Flex", + }, + }, { + about: "cpu-power", + cons: "cpu-power=2000", + expectedItypes: []string{ + "VM.Standard3.Flex", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", + }, }, { about: "mem", cons: "mem=4G", expectedItypes: []string{ - "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", + "VM.Standard3.Flex", "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", }, }, { about: "root-disk", cons: "root-disk=16G", expectedItypes: []string{ - "m1.medium", "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", + "VM.Standard3.Flex", "m1.medium", "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge", }, }, { about: "arches filtered by constraint", @@ -146,8 +162,8 @@ var getInstanceTypesTest = []struct { cons: "cores=4", itypesToUse: []InstanceType{ {Id: "5", Name: "it-5", Arch: "amd64", Mem: 1024, CpuCores: 2}, - {Id: "4", Name: "it-4", Arch: "amd64", Mem: 2048, CpuCores: 4}, - {Id: "3", Name: "it-3", Arch: "amd64", Mem: 1024, CpuCores: 4}, + {Id: "4", Name: "it-4", Arch: "amd64", Mem: 4096, CpuCores: 4}, + {Id: "3", Name: "it-3", Arch: "amd64", Mem: 2048, CpuCores: 4}, {Id: "2", Name: "it-2", Arch: "amd64", Mem: 256, CpuCores: 4}, {Id: "1", Name: "it-1", Arch: "amd64", Mem: 512, CpuCores: 4}, }, @@ -157,9 +173,9 @@ var getInstanceTypesTest = []struct { about: "small mem specified, use that even though less than needed for mongodb", cons: "mem=300M", itypesToUse: []InstanceType{ - {Id: "3", Name: "it-3", Arch: "amd64", Mem: 2048}, - {Id: "2", Name: "it-2", Arch: "amd64", Mem: 256}, - {Id: "1", Name: "it-1", Arch: "amd64", Mem: 512}, + {Id: "3", Name: "it-3", Arch: "amd64", Mem: 2048, CpuCores: 1}, + {Id: "2", Name: "it-2", Arch: "amd64", Mem: 256, CpuCores: 1}, + {Id: "1", Name: "it-1", Arch: "amd64", Mem: 512, CpuCores: 1}, }, expectedItypes: []string{"it-1", "it-3"}, }, @@ -167,10 +183,10 @@ var getInstanceTypesTest = []struct { about: "mem specified and match found", cons: "mem=4G arch=amd64", itypesToUse: []InstanceType{ - {Id: "4", Name: "it-4", Arch: "arm64", Mem: 8096}, - {Id: "3", Name: "it-3", Arch: "amd64", Mem: 4096}, - {Id: "2", Name: "it-2", Arch: "amd64", Mem: 2048}, - {Id: "1", Name: "it-1", Arch: "amd64", Mem: 512}, + {Id: "4", Name: "it-4", Arch: "arm64", Mem: 8096, CpuCores: 1}, + {Id: "3", Name: "it-3", Arch: "amd64", Mem: 4096, CpuCores: 1}, + {Id: "2", Name: "it-2", Arch: "amd64", Mem: 2048, CpuCores: 1}, + {Id: "1", Name: "it-1", Arch: "amd64", Mem: 512, CpuCores: 1}, }, expectedItypes: []string{"it-3"}, }, @@ -245,8 +261,8 @@ func (s *instanceTypeSuite) TestGetMatchingInstanceTypesErrors(c *gc.C) { _, err = MatchingInstanceTypes(instanceTypes, "test", constraints.MustParse("cores=9000")) c.Check(err, gc.ErrorMatches, `no instance types in test matching constraints "cores=9000"`) - _, err = MatchingInstanceTypes(instanceTypes, "test", constraints.MustParse("mem=90000M")) - c.Check(err, gc.ErrorMatches, `no instance types in test matching constraints "mem=90000M"`) + _, err = MatchingInstanceTypes(instanceTypes, "test", constraints.MustParse("mem=900000M")) + c.Check(err, gc.ErrorMatches, `no instance types in test matching constraints "mem=900000M"`) _, err = MatchingInstanceTypes(instanceTypes, "test", constraints.MustParse("instance-type=dep.medium mem=8G")) c.Check(err, gc.ErrorMatches, `no instance types in test matching constraints "instance-type=dep.medium mem=8192M"`) @@ -273,6 +289,15 @@ var instanceTypeMatchTests = []struct { {"cpu-power=9001", "cc2.8xlarge", ""}, {"mem=1G", "t1.micro", ""}, {"arch=arm64", "c1.xlarge", ""}, + + // Match on flexible shape against their maximum cpu-cores + {"cores=1", "VM.Standard3.Flex", "amd64"}, + {"cores=16", "VM.Standard3.Flex", "amd64"}, + {"cores=31", "VM.Standard3.Flex", "amd64"}, + // Match on flexible shape against their maximum memory + {"mem=1G", "VM.Standard3.Flex", "amd64"}, + {"mem=16G", "VM.Standard3.Flex", "amd64"}, + {"mem=511G", "VM.Standard3.Flex", "amd64"}, } func (s *instanceTypeSuite) TestMatch(c *gc.C) { @@ -459,3 +484,7 @@ func (s *instanceTypeSuite) TestSortByName(c *gc.C) { c.Check(names, gc.DeepEquals, t.expectedItypes) } } + +func makeUint64Pointer(val uint64) *uint64 { + return &val +} diff --git a/provider/azure/environ_test.go b/provider/azure/environ_test.go index a96792560d7..8b5ce4c8c6d 100644 --- a/provider/azure/environ_test.go +++ b/provider/azure/environ_test.go @@ -2093,7 +2093,7 @@ func (s *environSuite) TestInstanceInformation(c *gc.C) { s.sender = s.startInstanceSenders(startInstanceSenderParams{bootstrap: false}) types, err := env.InstanceTypes(s.callCtx, constraints.Value{}) c.Assert(err, jc.ErrorIsNil) - c.Assert(types.InstanceTypes, gc.HasLen, 6) + c.Assert(types.InstanceTypes, gc.HasLen, 4) cons := constraints.MustParse("mem=4G") types, err = env.InstanceTypes(s.callCtx, cons) diff --git a/provider/ec2/image_test.go b/provider/ec2/image_test.go index d2c90240d14..16229645887 100644 --- a/provider/ec2/image_test.go +++ b/provider/ec2/image_test.go @@ -53,6 +53,7 @@ var testInstanceTypes = []instances.InstanceType{{ Cost: 21, }, { Name: "t3a.medium", + CpuCores: 2, CpuPower: instances.CpuPower(700), Mem: 4096, Arch: "amd64", @@ -120,7 +121,7 @@ var findInstanceSpecTests = []struct { { version: "18.04", arch: "amd64", - itype: "t3a.micro", + itype: "t3a.small", image: "ami-00001133", }, { version: "18.04", @@ -144,13 +145,13 @@ var findInstanceSpecTests = []struct { version: "18.04", arch: "amd64", cons: "mem=", - itype: "t3a.nano", + itype: "t3a.small", image: "ami-00001133", }, { version: "18.04", arch: "amd64", cons: "cpu-power=", - itype: "t3a.micro", + itype: "t3a.small", image: "ami-00001133", }, { version: "18.04", @@ -194,13 +195,13 @@ var findInstanceSpecTests = []struct { }, { version: "22.04", arch: "amd64", - itype: "t3a.micro", + itype: "t3a.small", image: "ami-02204133", }, { version: "20.04", arch: "amd64", cons: "arch=amd64", - itype: "t3a.micro", + itype: "t3a.small", image: "ami-02004133", }, { version: "20.04", diff --git a/provider/gce/environ_instance_test.go b/provider/gce/environ_instance_test.go index 1cd5302c6be..69132e1d7a3 100644 --- a/provider/gce/environ_instance_test.go +++ b/provider/gce/environ_instance_test.go @@ -216,7 +216,7 @@ func (s *environInstSuite) TestListMachineTypes(c *gc.C) { // If no zone is specified, no machine types will be pulled. s.FakeConn.Zones = nil _, err := s.Env.InstanceTypes(s.CallCtx, constraints.Value{}) - c.Assert(err, gc.ErrorMatches, "no instance types in matching constraints \"cores=2 mem=2048M\"") + c.Assert(err, gc.ErrorMatches, "no instance types in matching constraints.*") // If a non-empty list of zones is specified , we will make an API call // to fetch the available machine types. diff --git a/provider/gce/instance_information.go b/provider/gce/instance_information.go index 6ca1544e27d..67e96ccba26 100644 --- a/provider/gce/instance_information.go +++ b/provider/gce/instance_information.go @@ -25,17 +25,18 @@ var ( // minCpuCores is the assumed minimum CPU cores we prefer in order to run a server. minCpuCores uint64 = 2 - - // minMemoryHeuristic is the assumed minimum amount of memory (in MB) we prefer in order to run a server (2GB) - minMemoryHeuristic uint64 = 2048 ) +// ensureDefaultConstraints adds the minimum number of cpu cores value to the +// constraints if the user has not provided a cpu-cores constraint. +// This function exists only so that the minimum cpu cores takes precedence +// over the default cpu-cores and memory values implemented in +// instances.MatchingInstanceTypes() func ensureDefaultConstraints(c constraints.Value) constraints.Value { - if c.HasInstanceType() || c.HasCpuCores() || c.HasMem() { + if c.HasInstanceType() || c.HasCpuCores() { return c } c.CpuCores = &minCpuCores - c.Mem = &minMemoryHeuristic return c } diff --git a/provider/gce/instance_information_test.go b/provider/gce/instance_information_test.go index cc18050278a..427632f412f 100644 --- a/provider/gce/instance_information_test.go +++ b/provider/gce/instance_information_test.go @@ -55,7 +55,7 @@ func (s *instanceInformationSuite) TestEnsureDefaultConstraints(c *gc.C) { cons := constraints.Value{} c.Assert(cons.String(), gc.Equals, ``) cons = ensureDefaultConstraints(cons) - c.Assert(cons.String(), gc.Equals, `cores=2 mem=2048M`) + c.Assert(cons.String(), gc.Equals, `cores=2`) var err error // Do not fill default cores and mem if instance type is provided. @@ -84,5 +84,5 @@ func (s *instanceInformationSuite) TestEnsureDefaultConstraints(c *gc.C) { cons, err = constraints.Parse(`mem=4096M`) c.Assert(err, jc.ErrorIsNil) cons = ensureDefaultConstraints(cons) - c.Assert(cons.String(), gc.Equals, `mem=4096M`) + c.Assert(cons.String(), gc.Equals, `cores=2 mem=4096M`) } diff --git a/provider/oci/common_test.go b/provider/oci/common_test.go index 9924cb795e2..d8f52fcc49f 100644 --- a/provider/oci/common_test.go +++ b/provider/oci/common_test.go @@ -231,8 +231,31 @@ func listShapesResponse() []ociCore.Shape { BillingType: "PAID", }, { - Shape: makeStringPointer("VM.Standard.A1.Flex"), - ProcessorDescription: makeStringPointer("3.0 GHz Ampere® Altra™"), + Shape: makeStringPointer("VM.Standard.A1.Flex"), + ProcessorDescription: makeStringPointer("3.0 GHz Ampere® Altra™"), + OcpuOptions: &ociCore.ShapeOcpuOptions{ + Max: makeFloat32Pointer(80), + }, + MemoryOptions: &ociCore.ShapeMemoryOptions{ + MaxInGBs: makeFloat32Pointer(512), + }, + Ocpus: makeFloat32Pointer(1), + MemoryInGBs: makeFloat32Pointer(6), + LocalDisks: makeIntPointer(0), + LocalDisksTotalSizeInGBs: (*float32)(nil), + PlatformConfigOptions: (*ociCore.ShapePlatformConfigOptions)(nil), + IsBilledForStoppedInstance: makeBoolPointer(false), + BillingType: "LIMITED_FREE", + }, + { + Shape: makeStringPointer("VM.Standard3.Flex"), + ProcessorDescription: makeStringPointer("2.0 GHz Intel® Xeon® Platinum 8167M (Skylake)"), + OcpuOptions: &ociCore.ShapeOcpuOptions{ + Max: makeFloat32Pointer(32), + }, + MemoryOptions: &ociCore.ShapeMemoryOptions{ + MaxInGBs: makeFloat32Pointer(512), + }, Ocpus: makeFloat32Pointer(1), MemoryInGBs: makeFloat32Pointer(6), LocalDisks: makeIntPointer(0), diff --git a/provider/oci/environ.go b/provider/oci/environ.go index 9329854366c..f71d7e28ae6 100644 --- a/provider/oci/environ.go +++ b/provider/oci/environ.go @@ -617,6 +617,25 @@ func (e *Environ) startInstance( FreeformTags: tags, } + // If the selected spec is a flexible shape, we must provide the number + // of OCPUs at least, so if the user hasn't provided cpu constraints we + // must pass the default value. + if (spec.InstanceType.MaxCpuCores != nil && spec.InstanceType.MaxCpuCores != &spec.InstanceType.CpuCores) || + (spec.InstanceType.MaxMem != nil && spec.InstanceType.MaxMem != &spec.InstanceType.Mem) { + instanceDetails.ShapeConfig = &ociCore.LaunchInstanceShapeConfigDetails{} + if args.Constraints.HasCpuCores() { + cpuCores := float32(*args.Constraints.CpuCores) + instanceDetails.ShapeConfig.Ocpus = &cpuCores + } else { + cpuCores := float32(instances.MinCpuCores) + instanceDetails.ShapeConfig.Ocpus = &cpuCores + } + if args.Constraints.HasMem() { + mem := float32(*args.Constraints.Mem) + instanceDetails.ShapeConfig.MemoryInGBs = &mem + } + } + request := ociCore.LaunchInstanceRequest{ LaunchInstanceDetails: instanceDetails, } diff --git a/provider/oci/environ_test.go b/provider/oci/environ_test.go index 9415db9e44b..8f9914c944e 100644 --- a/provider/oci/environ_test.go +++ b/provider/oci/environ_test.go @@ -920,6 +920,24 @@ func (s *environSuite) TestBootstrap(c *gc.C) { c.Assert(err, gc.IsNil) } +func (s *environSuite) TestBootstrapFlexibleShape(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStartInstanceExpectations(true, true, gomock.Any()) + + ctx := envtesting.BootstrapTODOContext(c) + _, err := s.env.Bootstrap(ctx, nil, + environs.BootstrapParams{ + ControllerConfig: testing.FakeControllerConfig(), + AvailableTools: makeToolsList("ubuntu"), + BootstrapSeries: "jammy", + SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, + BootstrapConstraints: constraints.MustParse("cpu-cores=16"), + }) + c.Assert(err, gc.IsNil) +} + type noPublicIPMatcher struct{} func (noPublicIPMatcher) Matches(arg interface{}) bool { diff --git a/provider/oci/images.go b/provider/oci/images.go index 80945ba2df6..567d59ab817 100644 --- a/provider/oci/images.go +++ b/provider/oci/images.go @@ -291,6 +291,11 @@ func NewInstanceImage(img ociCore.Image, compartmentID *string) (imgType Instanc return imgType, nil } +// instanceTypes will return the list of instanceTypes with information +// retrieved from OCI shapes supported by the imageID and compartmentID. +// TODO(nvinuesa) 2023-09-26 +// We should only keep flexible shapes as OCI recommends: +// https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm#flexible#previous-generation-shapes__previous-generation-vm#ariaid-title18 func instanceTypes(cli ComputeClient, compartmentID, imageID *string) ([]instances.InstanceType, error) { if cli == nil { return nil, errors.Errorf("cannot use nil client") @@ -325,6 +330,18 @@ func instanceTypes(cli ComputeClient, compartmentID, imageID *string) ([]instanc CpuCores: uint64(cpus), VirtType: &instanceType, } + // If the shape is a flexible shape then the MemoryOptions and + // OcpuOptions will not be nil and they indicate the maximum + // and minimum values. We assign the max memory and cpu cores + // values to the instance type in that case. + if val.MemoryOptions != nil { + maxMem := uint64(*val.MemoryOptions.MaxInGBs) * 1024 + newType.MaxMem = &maxMem + } + if val.OcpuOptions != nil { + maxCpuCores := uint64(*val.OcpuOptions.Max) + newType.MaxCpuCores = &maxCpuCores + } types = append(types, newType) } return types, nil @@ -428,6 +445,7 @@ func refreshImageCache(cli ComputeClient, compartmentID *string) (*ImageCache, e images := map[corebase.Base][]InstanceImage{} for _, val := range items { + logger.Warningf("*** LISTING SHAPES FOR %s", val.String()) instTypes, err := instanceTypes(cli, compartmentID, val.Id) if err != nil { return nil, err diff --git a/provider/oci/images_test.go b/provider/oci/images_test.go index 34a69c5b5a8..71fe6fbeb61 100644 --- a/provider/oci/images_test.go +++ b/provider/oci/images_test.go @@ -87,6 +87,10 @@ func makeIntPointer(name int) *int { return &name } +func makeUint64Pointer(name uint64) *uint64 { + return &name +} + func makeBoolPointer(name bool) *bool { return &name } @@ -103,7 +107,7 @@ func (s *imagesSuite) TestInstanceTypes(c *gc.C) { types, err := oci.InstanceTypes(compute, &s.testCompartment, &s.testImageID) c.Assert(err, gc.IsNil) - c.Check(types, gc.HasLen, 4) + c.Check(types, gc.HasLen, 5) expectedTypes := []instances.InstanceType{ { Name: "VM.Standard1.1", @@ -124,11 +128,21 @@ func (s *imagesSuite) TestInstanceTypes(c *gc.C) { CpuCores: 160, VirtType: makeStringPointer("metal"), }, { - Name: "VM.Standard.A1.Flex", - Arch: arch.ARM64, - Mem: 6 * 1024, - CpuCores: 1, - VirtType: makeStringPointer("vm"), + Name: "VM.Standard.A1.Flex", + Arch: arch.ARM64, + Mem: 6 * 1024, + MaxCpuCores: makeUint64Pointer(80), + MaxMem: makeUint64Pointer(512 * 1024), + CpuCores: 1, + VirtType: makeStringPointer("vm"), + }, { + Name: "VM.Standard3.Flex", + Arch: arch.AMD64, + Mem: 6 * 1024, + MaxCpuCores: makeUint64Pointer(32), + MaxMem: makeUint64Pointer(512 * 1024), + CpuCores: 1, + VirtType: makeStringPointer("vm"), }, } c.Assert(types, gc.DeepEquals, expectedTypes) @@ -224,7 +238,7 @@ func (s *imagesSuite) TestRefreshImageCache(c *gc.C) { c.Assert(imageMap[jammy][0].Version.TimeStamp, gc.Equals, timeStamp) // Check that InstanceTypes are set - c.Assert(imageMap[jammy][0].InstanceTypes, gc.HasLen, 4) + c.Assert(imageMap[jammy][0].InstanceTypes, gc.HasLen, 5) c.Assert(imageMap[corebase.MakeDefaultBase("centos", "7")][0].InstanceTypes, gc.HasLen, 2) } diff --git a/provider/oci/provider.go b/provider/oci/provider.go index be4abd767a6..9a885aa381e 100644 --- a/provider/oci/provider.go +++ b/provider/oci/provider.go @@ -41,27 +41,6 @@ type environConfig struct { var _ config.ConfigSchemaSource = (*EnvironProvider)(nil) var _ environs.ProviderSchema = (*EnvironProvider)(nil) -// var cloudSchema = &jsonschema.Schema{ -// Type: []jsonschema.Type{jsonschema.ObjectType}, -// Required: []string{cloud.RegionsKey, cloud.AuthTypesKey}, -// Order: []string{cloud.RegionsKey, cloud.AuthTypesKey}, -// Properties: map[string]*jsonschema.Schema{ -// cloud.RegionsKey: { -// Type: []jsonschema.Type{jsonschema.ObjectType}, -// Singular: "region", -// Plural: "regions", -// AdditionalProperties: &jsonschema.Schema{ -// Type: []jsonschema.Type{jsonschema.ObjectType}, -// }, -// }, -// cloud.AuthTypesKey: { -// // don't need a prompt, since there's only one choice. -// Type: []jsonschema.Type{jsonschema.ArrayType}, -// Enum: []interface{}{[]string{string(cloud.HTTPSigAuthType)}}, -// }, -// }, -// } - var configSchema = environschema.Fields{ "compartment-id": { Description: "The OCID of the compartment in which juju has access to create resources.", From 7c938cddfef62b4ae27595c7ca1bc90e1bab3dcb Mon Sep 17 00:00:00 2001 From: Nicolas Vinuesa Date: Mon, 2 Oct 2023 16:19:31 +0200 Subject: [PATCH 2/3] Add ensure shape config as a method, rename oci black box tests This commit adds a ensureShapeConfig method to make sure we are setting the correct shape config before launching an OCI instance. Add white box unit tests on provider/oci and therefore rename all the current tests to *_integration_test.go. --- environs/instances/instancetype.go | 9 +- ...mon_test.go => common_integration_test.go} | 0 provider/oci/environ.go | 48 +- provider/oci/environ_integration_test.go | 1279 ++++++++++++++++ provider/oci/environ_test.go | 1287 ++--------------- provider/oci/images.go | 13 +- ...ges_test.go => images_integration_test.go} | 0 ...e_test.go => instance_integration_test.go} | 0 ...test.go => networking_integration_test.go} | 0 ...r_test.go => provider_integration_test.go} | 0 ...ge_test.go => storage_integration_test.go} | 0 ...go => storage_volumes_integration_test.go} | 0 12 files changed, 1407 insertions(+), 1229 deletions(-) rename provider/oci/{common_test.go => common_integration_test.go} (100%) create mode 100644 provider/oci/environ_integration_test.go rename provider/oci/{images_test.go => images_integration_test.go} (100%) rename provider/oci/{instance_test.go => instance_integration_test.go} (100%) rename provider/oci/{networking_test.go => networking_integration_test.go} (100%) rename provider/oci/{provider_test.go => provider_integration_test.go} (100%) rename provider/oci/{storage_test.go => storage_integration_test.go} (100%) rename provider/oci/{storage_volumes_test.go => storage_volumes_integration_test.go} (100%) diff --git a/environs/instances/instancetype.go b/environs/instances/instancetype.go index d2a9857f876..334bbecc0e7 100644 --- a/environs/instances/instancetype.go +++ b/environs/instances/instancetype.go @@ -22,9 +22,12 @@ type InstanceType struct { Cost uint64 RootDisk uint64 // These attributes are not supported by all clouds. - VirtType *string // The type of virtualisation used by the hypervisor, must match the image. - CpuPower *uint64 - Tags []string + VirtType *string // The type of virtualisation used by the hypervisor, must match the image. + CpuPower *uint64 + Tags []string + // These two values are needed to know the maximum value of cpu and + // memory on flexible/custom instances. Currently only supported on + // OCI. MaxCpuCores *uint64 MaxMem *uint64 } diff --git a/provider/oci/common_test.go b/provider/oci/common_integration_test.go similarity index 100% rename from provider/oci/common_test.go rename to provider/oci/common_integration_test.go diff --git a/provider/oci/environ.go b/provider/oci/environ.go index f71d7e28ae6..9d62f9b745e 100644 --- a/provider/oci/environ.go +++ b/provider/oci/environ.go @@ -617,24 +617,7 @@ func (e *Environ) startInstance( FreeformTags: tags, } - // If the selected spec is a flexible shape, we must provide the number - // of OCPUs at least, so if the user hasn't provided cpu constraints we - // must pass the default value. - if (spec.InstanceType.MaxCpuCores != nil && spec.InstanceType.MaxCpuCores != &spec.InstanceType.CpuCores) || - (spec.InstanceType.MaxMem != nil && spec.InstanceType.MaxMem != &spec.InstanceType.Mem) { - instanceDetails.ShapeConfig = &ociCore.LaunchInstanceShapeConfigDetails{} - if args.Constraints.HasCpuCores() { - cpuCores := float32(*args.Constraints.CpuCores) - instanceDetails.ShapeConfig.Ocpus = &cpuCores - } else { - cpuCores := float32(instances.MinCpuCores) - instanceDetails.ShapeConfig.Ocpus = &cpuCores - } - if args.Constraints.HasMem() { - mem := float32(*args.Constraints.Mem) - instanceDetails.ShapeConfig.MemoryInGBs = &mem - } - } + ensureShapeConfig(spec.InstanceType, args.Constraints, &instanceDetails) request := ociCore.LaunchInstanceRequest{ LaunchInstanceDetails: instanceDetails, @@ -672,6 +655,35 @@ func (e *Environ) startInstance( return result, nil } +func ensureShapeConfig( + instanceSpec instances.InstanceType, + constraints constraints.Value, + instanceDetails *ociCore.LaunchInstanceDetails) { + + // If the selected spec is a flexible shape, we must provide the number + // of OCPUs at least, so if the user hasn't provided cpu constraints we + // must pass the default value. + if (instanceSpec.MaxCpuCores != nil && instanceSpec.MaxCpuCores != &instanceSpec.CpuCores) || + (instanceSpec.MaxMem != nil && instanceSpec.MaxMem != &instanceSpec.Mem) { + instanceDetails.ShapeConfig = &ociCore.LaunchInstanceShapeConfigDetails{} + if constraints.HasCpuCores() { + cpuCores := float32(*constraints.CpuCores) + instanceDetails.ShapeConfig.Ocpus = &cpuCores + } else { + cpuCores := float32(instances.MinCpuCores) + instanceDetails.ShapeConfig.Ocpus = &cpuCores + } + // If we don't set the memory on ShapeConfig, OCI uses a + // default value of memory per Ocpu core. For example, for the + // VM.Standard.A1.Flex, if we set 2 Ocpus OCI will set 12GB of + // memory (default is 6GB per core). + if constraints.HasMem() { + mem := float32(*constraints.Mem) + instanceDetails.ShapeConfig.MemoryInGBs = &mem + } + } +} + // StopInstances implements environs.InstanceBroker. func (e *Environ) StopInstances(ctx envcontext.ProviderCallContext, ids ...instance.Id) error { ociInstances, err := e.getOciInstances(ctx, ids...) diff --git a/provider/oci/environ_integration_test.go b/provider/oci/environ_integration_test.go new file mode 100644 index 00000000000..29579349641 --- /dev/null +++ b/provider/oci/environ_integration_test.go @@ -0,0 +1,1279 @@ +// Copyright 2018 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package oci_test + +import ( + "context" + "fmt" + "net/http" + + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + ociCore "github.com/oracle/oci-go-sdk/v65/core" + ociIdentity "github.com/oracle/oci-go-sdk/v65/identity" + "go.uber.org/mock/gomock" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/core/constraints" + "github.com/juju/juju/core/instance" + "github.com/juju/juju/environs" + envcontext "github.com/juju/juju/environs/context" + "github.com/juju/juju/environs/tags" + envtesting "github.com/juju/juju/environs/testing" + "github.com/juju/juju/provider/oci" + "github.com/juju/juju/testing" +) + +type environSuite struct { + commonSuite + + listInstancesResponse []ociCore.Instance +} + +var _ = gc.Suite(&environSuite{}) + +func (s *environSuite) SetUpTest(c *gc.C) { + s.commonSuite.SetUpTest(c) + *oci.MaxPollIterations = 2 + s.listInstancesResponse = + []ociCore.Instance{ + { + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeInstance1"), + LifecycleState: ociCore.InstanceLifecycleStateRunning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("fakeName"), + FreeformTags: s.tags, + }, + { + AvailabilityDomain: makeStringPointer("fakeZone2"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeInstance2"), + LifecycleState: ociCore.InstanceLifecycleStateRunning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("fakeName2"), + FreeformTags: s.tags, + }, + } + +} + +func (s *environSuite) setupAvailabilityDomainsExpectations(times int) { + request, response := makeListAvailabilityDomainsRequestResponse([]ociIdentity.AvailabilityDomain{ + { + Name: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + }, + { + Name: makeStringPointer("fakeZone2"), + CompartmentId: &s.testCompartment, + }, + { + Name: makeStringPointer("fakeZone3"), + CompartmentId: &s.testCompartment, + }, + }) + + expect := s.ident.EXPECT().ListAvailabilityDomains(context.Background(), request).Return(response, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func makeVcnName(controllerUUID, modelUUID string) string { + return fmt.Sprintf("%s-%s-%s", oci.VcnNamePrefix, controllerUUID, modelUUID) +} + +func (s *environSuite) setupVcnExpectations(vcnId string, t map[string]string, times int) { + vcnName := makeVcnName(t[tags.JujuController], t[tags.JujuModel]) + vcnResponse := []ociCore.Vcn{ + { + CompartmentId: &s.testCompartment, + CidrBlock: makeStringPointer(oci.DefaultAddressSpace), + Id: &vcnId, + LifecycleState: ociCore.VcnLifecycleStateAvailable, + DefaultRouteTableId: makeStringPointer("fakeRouteTable"), + DefaultSecurityListId: makeStringPointer("fakeSeclist"), + DisplayName: &vcnName, + FreeformTags: s.tags, + }, + } + + expect := s.netw.EXPECT().ListVcns(context.Background(), &s.testCompartment).Return(vcnResponse, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func (s *environSuite) setupSecurityListExpectations(vcnId string, t map[string]string, times int) { + name := fmt.Sprintf("juju-seclist-%s-%s", t[tags.JujuController], t[tags.JujuModel]) + request, response := makeListSecurityListsRequestResponse([]ociCore.SecurityList{ + { + CompartmentId: &s.testCompartment, + VcnId: &vcnId, + Id: makeStringPointer("fakeSecList"), + DisplayName: &name, + FreeformTags: t, + EgressSecurityRules: []ociCore.EgressSecurityRule{ + { + Destination: makeStringPointer(oci.AllowAllPrefix), + Protocol: makeStringPointer(oci.AllProtocols), + }, + }, + IngressSecurityRules: []ociCore.IngressSecurityRule{ + { + Source: makeStringPointer(oci.AllowAllPrefix), + Protocol: makeStringPointer(oci.AllProtocols), + }, + }, + }, + }) + expect := s.fw.EXPECT().ListSecurityLists(context.Background(), request.CompartmentId, &vcnId).Return(response.Items, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func (s *environSuite) setupInternetGatewaysExpectations(vcnId string, t map[string]string, times int) { + name := fmt.Sprintf("%s-%s", oci.InternetGatewayPrefix, t[tags.JujuController]) + enabled := true + request, response := makeListInternetGatewaysRequestResponse([]ociCore.InternetGateway{ + { + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeGwId"), + VcnId: &vcnId, + DisplayName: &name, + IsEnabled: &enabled, + }, + }) + expect := s.netw.EXPECT().ListInternetGateways(context.Background(), request.CompartmentId, request.VcnId).Return(response.Items, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func (s *environSuite) setupListRouteTableExpectations(vcnId string, t map[string]string, times int) { + name := fmt.Sprintf("%s-%s", oci.RouteTablePrefix, t[tags.JujuController]) + request, response := makeListRouteTableRequestResponse([]ociCore.RouteTable{ + { + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeRouteTableId"), + VcnId: &vcnId, + DisplayName: &name, + FreeformTags: t, + LifecycleState: ociCore.RouteTableLifecycleStateAvailable, + }, + }) + expect := s.netw.EXPECT().ListRouteTables(context.Background(), request.CompartmentId, request.VcnId).Return(response.Items, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func (s *environSuite) setupListSubnetsExpectations(vcnId, route string, t map[string]string, times int) { + zone1 := "fakeZone1" + zone2 := "fakeZone2" + zone3 := "fakeZone3" + displayNameZone1 := fmt.Sprintf("juju-%s-%s-%s", zone1, t[tags.JujuController], t[tags.JujuModel]) + displayNameZone2 := fmt.Sprintf("juju-%s-%s-%s", zone2, t[tags.JujuController], t[tags.JujuModel]) + displayNameZone3 := fmt.Sprintf("juju-%s-%s-%s", zone3, t[tags.JujuController], t[tags.JujuModel]) + response := []ociCore.Subnet{ + { + AvailabilityDomain: &zone1, + CidrBlock: makeStringPointer(oci.DefaultAddressSpace), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeSubnetId1"), + VcnId: &vcnId, + DisplayName: &displayNameZone1, + RouteTableId: &route, + LifecycleState: ociCore.SubnetLifecycleStateAvailable, + FreeformTags: t, + }, + { + AvailabilityDomain: &zone2, + CidrBlock: makeStringPointer(oci.DefaultAddressSpace), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeSubnetId2"), + VcnId: &vcnId, + DisplayName: &displayNameZone2, + RouteTableId: &route, + LifecycleState: ociCore.SubnetLifecycleStateAvailable, + FreeformTags: t, + }, + { + AvailabilityDomain: &zone3, + CidrBlock: makeStringPointer(oci.DefaultAddressSpace), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeSubnetId3"), + VcnId: &vcnId, + DisplayName: &displayNameZone3, + RouteTableId: &route, + LifecycleState: ociCore.SubnetLifecycleStateAvailable, + FreeformTags: t, + }, + } + + expect := s.netw.EXPECT().ListSubnets(context.Background(), &s.testCompartment, &vcnId).Return(response, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } +} + +func (s *environSuite) setupListImagesExpectations() { + response := []ociCore.Image{ + { + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeUbuntu1"), + OperatingSystem: makeStringPointer("Canonical Ubuntu"), + OperatingSystemVersion: makeStringPointer("22.04"), + DisplayName: makeStringPointer("Canonical-Ubuntu-22.04-2018.01.11-0"), + }, + { + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeUbuntu2"), + OperatingSystem: makeStringPointer("Canonical Ubuntu"), + OperatingSystemVersion: makeStringPointer("22.04"), + DisplayName: makeStringPointer("Canonical-Ubuntu-22.04-2018.01.12-0"), + }, + { + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeCentOS"), + OperatingSystem: makeStringPointer("CentOS"), + OperatingSystemVersion: makeStringPointer("7"), + DisplayName: makeStringPointer("CentOS-7-2017.10.19-0"), + }, + } + s.compute.EXPECT().ListImages(context.Background(), &s.testCompartment).Return(response, nil) + s.compute.EXPECT().ListShapes(context.Background(), gomock.Any(), gomock.Any()).Return(listShapesResponse(), nil).AnyTimes() +} + +func (s *environSuite) TestAvailabilityZones(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupAvailabilityDomainsExpectations(1) + + az, err := s.env.AvailabilityZones(nil) + c.Assert(err, gc.IsNil) + c.Check(len(az), gc.Equals, 3) +} + +func (s *environSuite) TestInstanceAvailabilityZoneNames(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil).Times(2) + + id := instance.Id("fakeInstance1") + req := []instance.Id{ + id, + } + zones, err := s.env.InstanceAvailabilityZoneNames(nil, req) + c.Assert(err, gc.IsNil) + c.Check(len(zones), gc.Equals, 1) + c.Assert(zones[id], gc.Equals, "fakeZone1") + + req = []instance.Id{ + instance.Id("fakeInstance1"), + instance.Id("fakeInstance3"), + } + zones, err = s.env.InstanceAvailabilityZoneNames(nil, req) + c.Assert(err, gc.ErrorMatches, "only some instances were found") + c.Check(len(zones), gc.Equals, 1) +} + +func (s *environSuite) TestInstances(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil).Times(2) + + req := []instance.Id{ + instance.Id("fakeInstance1"), + } + + inst, err := s.env.Instances(nil, req) + c.Assert(err, gc.IsNil) + c.Assert(len(inst), gc.Equals, 1) + c.Assert(inst[0].Id(), gc.Equals, instance.Id("fakeInstance1")) + + req = []instance.Id{ + instance.Id("fakeInstance1"), + instance.Id("fakeInstance3"), + } + inst, err = s.env.Instances(nil, req) + c.Assert(err, gc.ErrorMatches, "only some instances were found") + c.Check(len(inst), gc.Equals, 1) + c.Assert(inst[0].Id(), gc.Equals, instance.Id("fakeInstance1")) +} + +func (s *environSuite) TestPrepareForBootstrap(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupAvailabilityDomainsExpectations(1) + s.ident.EXPECT().ListAvailabilityDomains( + gomock.Any(), gomock.Any()).Return( + ociIdentity.ListAvailabilityDomainsResponse{}, errors.New("got error")) + + ctx := envtesting.BootstrapTODOContext(c) + err := s.env.PrepareForBootstrap(ctx, "controller-1") + c.Assert(err, gc.IsNil) + + err = s.env.PrepareForBootstrap(ctx, "controller-1") + c.Assert(err, gc.ErrorMatches, "got error") +} + +func (s *environSuite) TestCreate(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupAvailabilityDomainsExpectations(1) + s.ident.EXPECT().ListAvailabilityDomains( + gomock.Any(), gomock.Any()).Return( + ociIdentity.ListAvailabilityDomainsResponse{}, errors.New("got error")) + + err := s.env.Create(nil, environs.CreateParams{}) + c.Assert(err, gc.IsNil) + + err = s.env.Create(nil, environs.CreateParams{}) + c.Assert(err, gc.ErrorMatches, "got error") +} + +func (s *environSuite) TestConstraintsValidator(c *gc.C) { + validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) + c.Assert(err, jc.ErrorIsNil) + + cons := constraints.MustParse("arch=amd64") + unsupported, err := validator.Validate(cons) + c.Assert(err, jc.ErrorIsNil) + + c.Check(unsupported, gc.HasLen, 0) + +} + +func (s *environSuite) TestConstraintsValidatorEmpty(c *gc.C) { + validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) + c.Assert(err, jc.ErrorIsNil) + + unsupported, err := validator.Validate(constraints.Value{}) + c.Assert(err, jc.ErrorIsNil) + + c.Check(unsupported, gc.HasLen, 0) +} + +func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) { + validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) + c.Assert(err, jc.ErrorIsNil) + + cons := constraints.MustParse("arch=amd64 tags=foo virt-type=kvm") + unsupported, err := validator.Validate(cons) + c.Assert(err, jc.ErrorIsNil) + + c.Check(unsupported, jc.SameContents, []string{"tags", "virt-type"}) +} + +func (s *environSuite) TestConstraintsValidatorWrongArch(c *gc.C) { + validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) + c.Assert(err, jc.ErrorIsNil) + + cons := constraints.MustParse("arch=ppc64el") + _, err = validator.Validate(cons) + c.Check(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are:.*") +} + +func (s *environSuite) TestControllerInstancesNoControllerInstances(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil) + + ids, err := s.env.ControllerInstances(nil, s.controllerUUID) + c.Assert(err, gc.IsNil) + c.Check(len(ids), gc.Equals, 0) +} + +func (s *environSuite) TestControllerInstancesOneController(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.listInstancesResponse[0].FreeformTags = s.ctrlTags + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil) + + ids, err := s.env.ControllerInstances(nil, s.controllerUUID) + c.Assert(err, gc.IsNil) + c.Check(len(ids), gc.Equals, 1) +} + +func (s *environSuite) TestCloudInit(c *gc.C) { + cfg, err := oci.GetCloudInitConfig(s.env, "ubuntu", 1234, 4321) + c.Assert(err, jc.ErrorIsNil) + script, err := cfg.RenderScript() + c.Assert(err, jc.ErrorIsNil) + c.Check(script, jc.Contains, "/sbin/iptables -I INPUT -p tcp --dport 1234 -j ACCEPT") + c.Check(script, jc.Contains, "/sbin/iptables -I INPUT -p tcp --dport 4321 -j ACCEPT") + c.Check(script, jc.Contains, "/etc/init.d/netfilter-persistent save") + + cfg, err = oci.GetCloudInitConfig(s.env, "ubuntu", 0, 0) + c.Assert(err, jc.ErrorIsNil) + script, err = cfg.RenderScript() + c.Assert(err, jc.ErrorIsNil) + c.Check(script, gc.Not(jc.Contains), "/sbin/iptables -I INPUT -p tcp --dport 1234 -j ACCEPT") + c.Check(script, gc.Not(jc.Contains), "/sbin/iptables -I INPUT -p tcp --dport 4321 -j ACCEPT") + c.Check(script, gc.Not(jc.Contains), "/etc/init.d/netfilter-persistent save") + + cfg, err = oci.GetCloudInitConfig(s.env, "centos", 1234, 4321) + c.Assert(err, jc.ErrorIsNil) + script, err = cfg.RenderScript() + c.Assert(err, jc.ErrorIsNil) + c.Check(script, jc.Contains, "firewall-cmd --zone=public --add-port=1234/tcp --permanent") + c.Check(script, jc.Contains, "firewall-cmd --zone=public --add-port=4321/tcp --permanent") + c.Check(script, jc.Contains, "firewall-cmd --reload") +} + +type instanceTermination struct { + instanceId string + err error +} + +type ociInstanceTermination struct { + instance ociCore.Instance + err error +} + +func (s *environSuite) setupStopInstanceExpectations(instancesDetails []instanceTermination) { + exp := s.compute.EXPECT() + instancesListWithError := []ociInstanceTermination{} + instancesList := []ociCore.Instance{} + + for _, inst := range instancesDetails { + ociInstance := ociCore.Instance{ + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer(inst.instanceId), + LifecycleState: ociCore.InstanceLifecycleStateRunning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("fakeName"), + FreeformTags: s.tags, + } + instancesListWithError = append( + instancesListWithError, + ociInstanceTermination{ + instance: ociInstance, + err: inst.err}) + instancesList = append(instancesList, ociInstance) + } + + _, listInstancesResponse := makeListInstancesRequestResponse(instancesList) + + listInstancesResponse.RawResponse = &http.Response{ + StatusCode: 200, + } + + listCall := exp.ListInstances( + context.Background(), &s.testCompartment).Return( + listInstancesResponse.Items, nil).AnyTimes() + + for _, inst := range instancesListWithError { + requestMachine, responseMachine := makeGetInstanceRequestResponse(inst.instance) + + responseMachine.RawResponse = &http.Response{ + StatusCode: 200, + } + + terminateRequestMachine := ociCore.TerminateInstanceRequest{ + InstanceId: inst.instance.Id, + } + + terminateResponse := ociCore.TerminateInstanceResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + + terminatingInst := inst.instance + terminatingInst.LifecycleState = ociCore.InstanceLifecycleStateTerminating + requestMachineTerminating, responseMachineTerminating := makeGetInstanceRequestResponse(terminatingInst) + + terminatedInst := inst.instance + terminatedInst.LifecycleState = ociCore.InstanceLifecycleStateTerminated + requestMachineTerminated, responseMachineTerminated := makeGetInstanceRequestResponse(terminatedInst) + + getCall := exp.GetInstance( + context.Background(), requestMachine).Return( + responseMachine, nil).AnyTimes().After(listCall) + + terminateCall := exp.TerminateInstance( + context.Background(), terminateRequestMachine).Return( + terminateResponse, inst.err).After(getCall) + + if inst.err == nil { + terminatingCall := exp.GetInstance( + context.Background(), requestMachineTerminating).Return( + responseMachineTerminating, nil).Times(2).After(terminateCall) + exp.GetInstance( + context.Background(), requestMachineTerminated).Return( + responseMachineTerminated, nil).After(terminatingCall) + } else { + exp.GetInstance( + context.Background(), requestMachine).Return( + responseMachine, nil).AnyTimes().After(terminateCall) + } + } +} + +func (s *environSuite) TestStopInstances(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStopInstanceExpectations( + []instanceTermination{ + { + instanceId: "instance1", + err: nil, + }, + }, + ) + + ids := []instance.Id{ + instance.Id("instance1"), + } + err := s.env.StopInstances(nil, ids...) + c.Assert(err, gc.IsNil) + +} + +func (s *environSuite) TestStopInstancesSingleFail(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStopInstanceExpectations( + []instanceTermination{ + { + instanceId: "fakeInstance1", + err: nil, + }, + { + instanceId: "fakeInstance2", + err: errors.Errorf("I failed to terminate"), + }, + { + instanceId: "fakeInstance3", + err: nil, + }, + }, + ) + + ids := []instance.Id{ + instance.Id("fakeInstance1"), + instance.Id("fakeInstance2"), + instance.Id("fakeInstance3"), + } + err := s.env.StopInstances(nil, ids...) + c.Assert(err, gc.ErrorMatches, "failed to stop instance fakeInstance2: I failed to terminate") + +} + +func (s *environSuite) TestStopInstancesMultipleFail(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStopInstanceExpectations( + []instanceTermination{ + { + instanceId: "fakeInstance1", + err: nil, + }, + { + instanceId: "fakeInstance2", + err: errors.Errorf("I failed to terminate fakeInstance2"), + }, + { + instanceId: "fakeInstance3", + err: nil, + }, + { + instanceId: "fakeInstance4", + err: errors.Errorf("I failed to terminate fakeInstance4"), + }, + }, + ) + + ids := []instance.Id{ + instance.Id("fakeInstance1"), + instance.Id("fakeInstance2"), + instance.Id("fakeInstance3"), + instance.Id("fakeInstance4"), + } + err := s.env.StopInstances(nil, ids...) + // order in which the instances are returned or fail is not guaranteed + c.Assert(err, gc.ErrorMatches, `failed to stop instances \[fakeInstance[24] fakeInstance[24]\]: \[I failed to terminate fakeInstance[24] I failed to terminate fakeInstance[24]\]`) + +} + +func (s *environSuite) TestStopInstancesTimeoutTransitioningToTerminating(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + listInstancesRequest, listInstancesResponse := makeListInstancesRequestResponse( + []ociCore.Instance{ + { + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeInstance1"), + LifecycleState: ociCore.InstanceLifecycleStateRunning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("fakeName"), + FreeformTags: s.tags, + }, + }, + ) + + requestMachine1, responseMachine1 := makeGetInstanceRequestResponse(ociCore.Instance{ + CompartmentId: listInstancesResponse.Items[0].CompartmentId, + AvailabilityDomain: listInstancesResponse.Items[0].AvailabilityDomain, + Id: listInstancesResponse.Items[0].Id, + Region: listInstancesResponse.Items[0].Region, + Shape: listInstancesResponse.Items[0].Shape, + DisplayName: listInstancesResponse.Items[0].DisplayName, + FreeformTags: listInstancesResponse.Items[0].FreeformTags, + LifecycleState: ociCore.InstanceLifecycleStateRunning, + }) + + //s.listInstancesResponse.RawResponse = &http.Response{ + // StatusCode: 200, + //} + responseMachine1.RawResponse = &http.Response{ + StatusCode: 200, + } + + terminateRequestMachine1 := ociCore.TerminateInstanceRequest{ + InstanceId: listInstancesResponse.Items[0].Id, + } + + terminateResponse := ociCore.TerminateInstanceResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + + gomock.InOrder( + s.compute.EXPECT().ListInstances( + context.Background(), listInstancesRequest.CompartmentId).Return( + listInstancesResponse.Items, nil), + s.compute.EXPECT().GetInstance( + context.Background(), requestMachine1).Return( + responseMachine1, nil), + s.compute.EXPECT().TerminateInstance( + context.Background(), terminateRequestMachine1).Return( + terminateResponse, nil), + s.compute.EXPECT().GetInstance( + context.Background(), requestMachine1).Return( + responseMachine1, nil).Times(3), + ) + + ids := []instance.Id{ + instance.Id("fakeInstance1"), + } + err := s.env.StopInstances(nil, ids...) + c.Check(err, gc.ErrorMatches, ".*Instance still in running state after 2 checks") + +} + +func (s *environSuite) TestStopInstancesTimeoutTransitioningToTerminated(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + listInstancesRequest, listInstancesResponse := makeListInstancesRequestResponse( + []ociCore.Instance{ + { + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeInstance2"), + LifecycleState: ociCore.InstanceLifecycleStateRunning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("fakeName"), + FreeformTags: s.tags, + }, + }, + ) + + requestMachine1, responseMachine1 := makeGetInstanceRequestResponse(ociCore.Instance{ + CompartmentId: listInstancesResponse.Items[0].CompartmentId, + AvailabilityDomain: listInstancesResponse.Items[0].AvailabilityDomain, + Id: listInstancesResponse.Items[0].Id, + Region: listInstancesResponse.Items[0].Region, + Shape: listInstancesResponse.Items[0].Shape, + DisplayName: listInstancesResponse.Items[0].DisplayName, + FreeformTags: listInstancesResponse.Items[0].FreeformTags, + LifecycleState: ociCore.InstanceLifecycleStateRunning, + }) + + listInstancesResponse.RawResponse = &http.Response{ + StatusCode: 200, + } + responseMachine1.RawResponse = &http.Response{ + StatusCode: 200, + } + + terminateRequestMachine1 := ociCore.TerminateInstanceRequest{ + InstanceId: listInstancesResponse.Items[0].Id, + } + + terminateResponse := ociCore.TerminateInstanceResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + + responseMachine1Terminating := responseMachine1 + responseMachine1Terminating.Instance.LifecycleState = ociCore.InstanceLifecycleStateTerminating + + gomock.InOrder( + s.compute.EXPECT().ListInstances( + context.Background(), listInstancesRequest.CompartmentId).Return( + listInstancesResponse.Items, nil), + s.compute.EXPECT().GetInstance( + context.Background(), requestMachine1).Return( + responseMachine1, nil), + s.compute.EXPECT().TerminateInstance( + context.Background(), terminateRequestMachine1).Return( + terminateResponse, nil), + s.compute.EXPECT().GetInstance( + context.Background(), requestMachine1).Return( + responseMachine1Terminating, nil).AnyTimes(), + ) + + ids := []instance.Id{ + instance.Id("fakeInstance2"), + } + err := s.env.StopInstances(nil, ids...) + c.Check(err, gc.ErrorMatches, ".*Timed out waiting for instance to transition from TERMINATING to TERMINATED") + +} + +func (s *environSuite) TestAllRunningInstances(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil) + + ids, err := s.env.AllRunningInstances(nil) + c.Assert(err, gc.IsNil) + c.Check(len(ids), gc.Equals, 2) +} + +func (s *environSuite) TestAllRunningInstancesExtraUnrelatedInstance(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + // This instance does not have the model tags. It should be ignored. + unrelatedInstance := newFakeOCIInstance( + "notRelated", s.testCompartment, ociCore.InstanceLifecycleStateRunning) + s.listInstancesResponse = append( + s.listInstancesResponse, *unrelatedInstance) + + s.compute.EXPECT().ListInstances( + context.Background(), &s.testCompartment).Return( + s.listInstancesResponse, nil) + + ids, err := s.env.AllRunningInstances(nil) + c.Assert(err, gc.IsNil) + c.Check(len(ids), gc.Equals, 2) +} + +func (s *environSuite) setupLaunchInstanceExpectations( + isController bool, tags map[string]string, publicIP bool, launchInstanceMatcher gomock.Matcher, +) { + inst := ociCore.Instance{ + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + Id: makeStringPointer("fakeInstanceId"), + LifecycleState: ociCore.InstanceLifecycleStateProvisioning, + Region: makeStringPointer("us-phoenix-1"), + Shape: makeStringPointer("VM.Standard1.1"), + DisplayName: makeStringPointer("juju-06f00d-0"), + FreeformTags: tags, + } + responseLaunch := ociCore.LaunchInstanceResponse{ + Instance: inst, + } + s.compute.EXPECT().LaunchInstance(context.Background(), launchInstanceMatcher).Return(responseLaunch, nil) + + getInst := inst + if isController { + getInst.LifecycleState = ociCore.InstanceLifecycleStateRunning + + } + getResponse := ociCore.GetInstanceResponse{ + Instance: getInst, + } + s.compute.EXPECT().GetInstance(context.Background(), gomock.Any()).Return(getResponse, nil) + + if isController { + vnicID := "fakeVnicId" + attachRequest, attachResponse := makeListVnicAttachmentsRequestResponse([]ociCore.VnicAttachment{ + { + Id: makeStringPointer("fakeAttachmentId"), + AvailabilityDomain: makeStringPointer("fake"), + CompartmentId: &s.testCompartment, + InstanceId: makeStringPointer("fakeInstanceId"), + LifecycleState: ociCore.VnicAttachmentLifecycleStateAttached, + DisplayName: makeStringPointer("fakeAttachmentName"), + NicIndex: makeIntPointer(0), + VnicId: &vnicID, + }, + }) + + vnicRequest, vnicResponse := makeGetVnicRequestResponse([]ociCore.GetVnicResponse{ + { + Vnic: ociCore.Vnic{ + Id: &vnicID, + PrivateIp: makeStringPointer("10.0.0.20"), + DisplayName: makeStringPointer("fakeVnicName"), + PublicIp: makeStringPointer("2.2.2.2"), + MacAddress: makeStringPointer("aa:aa:aa:aa:aa:aa"), + SubnetId: makeStringPointer("fakeSubnetId"), + LifecycleState: ociCore.VnicLifecycleStateAvailable, + }, + }, + }) + + // These calls are only expected if we assign a public IP. + // They occur when polling for the IP after the instance is started. + if publicIP { + s.compute.EXPECT().ListVnicAttachments(context.Background(), attachRequest.CompartmentId, makeStringPointer("fakeInstanceId")).Return(attachResponse.Items, nil) + s.netw.EXPECT().GetVnic(context.Background(), vnicRequest[0]).Return(vnicResponse[0], nil) + } + } +} + +func (s *environSuite) setupEnsureNetworksExpectations(vcnId string, machineTags map[string]string) { + s.setupAvailabilityDomainsExpectations(0) + s.setupVcnExpectations(vcnId, machineTags, 1) + s.setupSecurityListExpectations(vcnId, machineTags, 1) + s.setupInternetGatewaysExpectations(vcnId, machineTags, 1) + s.setupListRouteTableExpectations(vcnId, machineTags, 1) + s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 1) +} + +func (s *environSuite) setupStartInstanceExpectations( + isController bool, publicIP bool, launchInstanceMatcher gomock.Matcher) { + vcnId := "fakeVCNId" + machineTags := map[string]string{ + tags.JujuController: testing.ControllerTag.Id(), + tags.JujuModel: testing.ModelTag.Id(), + } + + if isController { + machineTags[tags.JujuIsController] = "true" + } + + s.setupEnsureNetworksExpectations(vcnId, machineTags) + s.setupListImagesExpectations() + s.setupLaunchInstanceExpectations(isController, machineTags, publicIP, launchInstanceMatcher) +} + +func (s *environSuite) TestBootstrap(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStartInstanceExpectations(true, true, gomock.Any()) + + ctx := envtesting.BootstrapTODOContext(c) + _, err := s.env.Bootstrap(ctx, nil, + environs.BootstrapParams{ + ControllerConfig: testing.FakeControllerConfig(), + AvailableTools: makeToolsList("ubuntu"), + BootstrapSeries: "jammy", + SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, + }) + c.Assert(err, gc.IsNil) +} + +func (s *environSuite) TestBootstrapFlexibleShape(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStartInstanceExpectations(true, true, gomock.Any()) + + // By setting the constraint cpu-cores=32, we are selecting the + // VM.Standard3.Flex shape defined in listShapesResponse(), which has + // 32 maximum CPUs. + ctx := envtesting.BootstrapTODOContext(c) + _, err := s.env.Bootstrap(ctx, nil, + environs.BootstrapParams{ + ControllerConfig: testing.FakeControllerConfig(), + AvailableTools: makeToolsList("ubuntu"), + BootstrapSeries: "jammy", + SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, + BootstrapConstraints: constraints.MustParse("cpu-cores=32"), + }) + c.Assert(err, gc.IsNil) +} + +type noPublicIPMatcher struct{} + +func (noPublicIPMatcher) Matches(arg interface{}) bool { + li := arg.(ociCore.LaunchInstanceRequest) + assign := *li.CreateVnicDetails.AssignPublicIp + return !assign +} + +func (noPublicIPMatcher) String() string { return "" } + +func (s *environSuite) TestBootstrapNoAllocatePublicIP(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + s.setupStartInstanceExpectations(true, false, noPublicIPMatcher{}) + + ctx := envtesting.BootstrapTODOContext(c) + _, err := s.env.Bootstrap(ctx, nil, + environs.BootstrapParams{ + ControllerConfig: testing.FakeControllerConfig(), + AvailableTools: makeToolsList("ubuntu"), + BootstrapSeries: "jammy", + SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, + BootstrapConstraints: constraints.MustParse("allocate-public-ip=false"), + }) + c.Assert(err, gc.IsNil) +} + +func (s *environSuite) TestBootstrapNoMatchingTools(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + vcnId := "fakeVCNId" + machineTags := map[string]string{ + tags.JujuController: testing.ControllerTag.Id(), + tags.JujuModel: testing.ModelTag.Id(), + tags.JujuIsController: "true", + } + + s.setupAvailabilityDomainsExpectations(0) + s.setupVcnExpectations(vcnId, machineTags, 0) + s.setupSecurityListExpectations(vcnId, machineTags, 0) + s.setupInternetGatewaysExpectations(vcnId, machineTags, 0) + s.setupListRouteTableExpectations(vcnId, machineTags, 0) + s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 0) + + ctx := envtesting.BootstrapTODOContext(c) + _, err := s.env.Bootstrap(ctx, nil, + environs.BootstrapParams{ + ControllerConfig: testing.FakeControllerConfig(), + AvailableTools: makeToolsList("centos"), + BootstrapSeries: "jammy", + SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, + }) + c.Assert(err, gc.ErrorMatches, "no matching agent binaries available") + +} + +func (s *environSuite) setupDeleteSecurityListExpectations(seclistId string, times int) { + request := ociCore.DeleteSecurityListRequest{ + SecurityListId: makeStringPointer(seclistId), + } + + response := ociCore.DeleteSecurityListResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + + expect := s.fw.EXPECT().DeleteSecurityList(context.Background(), request).Return(response, nil) + if times == 0 { + expect.AnyTimes() + } else { + expect.Times(times) + } + + requestGet := ociCore.GetSecurityListRequest{ + SecurityListId: makeStringPointer("fakeSecList"), + } + + seclist := ociCore.SecurityList{ + Id: makeStringPointer("fakeSecList"), + LifecycleState: ociCore.SecurityListLifecycleStateTerminated, + } + + responseGet := ociCore.GetSecurityListResponse{ + SecurityList: seclist, + } + + s.fw.EXPECT().GetSecurityList(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() + +} + +func (s *environSuite) setupDeleteSubnetExpectations(subnetIds []string) { + for _, id := range subnetIds { + request := ociCore.DeleteSubnetRequest{ + SubnetId: makeStringPointer(id), + } + + response := ociCore.DeleteSubnetResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + s.netw.EXPECT().DeleteSubnet(context.Background(), request).Return(response, nil).AnyTimes() + + requestGet := ociCore.GetSubnetRequest{ + SubnetId: makeStringPointer(id), + } + + subnet := ociCore.Subnet{ + Id: makeStringPointer("fakeSecList"), + LifecycleState: ociCore.SubnetLifecycleStateTerminated, + } + + responseGet := ociCore.GetSubnetResponse{ + Subnet: subnet, + } + + s.netw.EXPECT().GetSubnet(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() + } +} + +func (s *environSuite) setupDeleteRouteTableExpectations(vcnId, routeTableId string, t map[string]string) { + s.setupListRouteTableExpectations(vcnId, t, 1) + request := ociCore.DeleteRouteTableRequest{ + RtId: makeStringPointer(routeTableId), + } + + response := ociCore.DeleteRouteTableResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + s.netw.EXPECT().DeleteRouteTable(context.Background(), request).Return(response, nil).AnyTimes() + + requestGet := ociCore.GetRouteTableRequest{ + RtId: makeStringPointer(routeTableId), + } + + rt := ociCore.RouteTable{ + Id: makeStringPointer(routeTableId), + LifecycleState: ociCore.RouteTableLifecycleStateTerminated, + } + + responseGet := ociCore.GetRouteTableResponse{ + RouteTable: rt, + } + + s.netw.EXPECT().GetRouteTable(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() +} + +func (s *environSuite) setupDeleteInternetGatewayExpectations(vcnId, IgId string, t map[string]string) { + s.setupInternetGatewaysExpectations(vcnId, t, 1) + request := ociCore.DeleteInternetGatewayRequest{ + IgId: &IgId, + } + + response := ociCore.DeleteInternetGatewayResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + s.netw.EXPECT().DeleteInternetGateway(context.Background(), request).Return(response, nil) + + requestGet := ociCore.GetInternetGatewayRequest{ + IgId: &IgId, + } + + ig := ociCore.InternetGateway{ + Id: &IgId, + LifecycleState: ociCore.InternetGatewayLifecycleStateTerminated, + } + + responseGet := ociCore.GetInternetGatewayResponse{ + InternetGateway: ig, + } + + s.netw.EXPECT().GetInternetGateway(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() +} + +func (s *environSuite) setupDeleteVcnExpectations(vcnId string) { + request := ociCore.DeleteVcnRequest{ + VcnId: &vcnId, + } + + response := ociCore.DeleteVcnResponse{ + RawResponse: &http.Response{ + StatusCode: 201, + }, + } + s.netw.EXPECT().DeleteVcn(context.Background(), request).Return(response, nil) + + requestGet := ociCore.GetVcnRequest{ + VcnId: &vcnId, + } + + vcn := ociCore.Vcn{ + Id: &vcnId, + LifecycleState: ociCore.VcnLifecycleStateTerminated, + } + + responseGet := ociCore.GetVcnResponse{ + Vcn: vcn, + } + + s.netw.EXPECT().GetVcn(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() +} + +func (s *environSuite) setupDeleteVolumesExpectations() { + size := int64(50) + volumes := []ociCore.Volume{ + { + Id: makeStringPointer("fakeVolumeID1"), + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + DisplayName: makeStringPointer("fakeVolume1"), + LifecycleState: ociCore.VolumeLifecycleStateAvailable, + SizeInGBs: &size, + FreeformTags: map[string]string{ + tags.JujuController: s.controllerUUID, + }, + }, + { + Id: makeStringPointer("fakeVolumeID2"), + AvailabilityDomain: makeStringPointer("fakeZone1"), + CompartmentId: &s.testCompartment, + DisplayName: makeStringPointer("fakeVolume2"), + LifecycleState: ociCore.VolumeLifecycleStateAvailable, + SizeInGBs: &size, + FreeformTags: map[string]string{ + tags.JujuController: s.controllerUUID, + }, + }, + } + + copyVolumes := volumes + copyVolumes[0].LifecycleState = ociCore.VolumeLifecycleStateTerminated + copyVolumes[1].LifecycleState = ociCore.VolumeLifecycleStateTerminated + + listRequest := ociCore.ListVolumesRequest{ + CompartmentId: &s.testCompartment, + } + + listResponse := ociCore.ListVolumesResponse{ + Items: volumes, + } + + requestVolume1 := ociCore.GetVolumeRequest{ + VolumeId: makeStringPointer("fakeVolumeID1"), + } + + requestVolume2 := ociCore.GetVolumeRequest{ + VolumeId: makeStringPointer("fakeVolumeID2"), + } + + responseVolume1 := ociCore.GetVolumeResponse{ + Volume: copyVolumes[0], + } + + responseVolume2 := ociCore.GetVolumeResponse{ + Volume: copyVolumes[1], + } + + s.storage.EXPECT().ListVolumes(context.Background(), listRequest.CompartmentId).Return(listResponse.Items, nil).AnyTimes() + s.storage.EXPECT().GetVolume(context.Background(), requestVolume1).Return(responseVolume1, nil).AnyTimes() + s.storage.EXPECT().GetVolume(context.Background(), requestVolume2).Return(responseVolume2, nil).AnyTimes() +} + +func (s *environSuite) TestDestroyController(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + machineTags := map[string]string{ + tags.JujuController: testing.ControllerTag.Id(), + tags.JujuModel: testing.ModelTag.Id(), + } + + vcnId := "fakeVCNId" + s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 1) + s.setupStopInstanceExpectations( + []instanceTermination{ + { + instanceId: s.testInstanceID, + err: nil, + }, + }, + ) + s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateTerminated, 0) + s.setupVcnExpectations(vcnId, machineTags, 1) + s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 1) + s.setupSecurityListExpectations(vcnId, machineTags, 1) + s.setupDeleteRouteTableExpectations(vcnId, "fakeRouteTableId", machineTags) + s.setupDeleteSubnetExpectations([]string{"fakeSubnetId1", "fakeSubnetId2", "fakeSubnetId3"}) + s.setupDeleteSecurityListExpectations("fakeSecList", 0) + s.setupDeleteInternetGatewayExpectations(vcnId, "fakeGwId", machineTags) + s.setupDeleteVcnExpectations(vcnId) + s.setupDeleteVolumesExpectations() + + err := s.env.DestroyController(nil, s.controllerUUID) + c.Assert(err, gc.IsNil) +} + +func (s *environSuite) TestEnsureShapeConfig(c *gc.C) { + ctrl := s.patchEnv(c) + defer ctrl.Finish() + + machineTags := map[string]string{ + tags.JujuController: testing.ControllerTag.Id(), + tags.JujuModel: testing.ModelTag.Id(), + } + + vcnId := "fakeVCNId" + s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 1) + s.setupStopInstanceExpectations( + []instanceTermination{ + { + instanceId: s.testInstanceID, + err: nil, + }, + }, + ) + s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateTerminated, 0) + s.setupVcnExpectations(vcnId, machineTags, 1) + s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 1) + s.setupSecurityListExpectations(vcnId, machineTags, 1) + s.setupDeleteRouteTableExpectations(vcnId, "fakeRouteTableId", machineTags) + s.setupDeleteSubnetExpectations([]string{"fakeSubnetId1", "fakeSubnetId2", "fakeSubnetId3"}) + s.setupDeleteSecurityListExpectations("fakeSecList", 0) + s.setupDeleteInternetGatewayExpectations(vcnId, "fakeGwId", machineTags) + s.setupDeleteVcnExpectations(vcnId) + s.setupDeleteVolumesExpectations() + + err := s.env.DestroyController(nil, s.controllerUUID) + c.Assert(err, gc.IsNil) +} diff --git a/provider/oci/environ_test.go b/provider/oci/environ_test.go index 8f9914c944e..bde27aa20f0 100644 --- a/provider/oci/environ_test.go +++ b/provider/oci/environ_test.go @@ -1,1242 +1,123 @@ -// Copyright 2018 Canonical Ltd. +// Copyright 2023 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package oci_test +package oci import ( - "context" - "fmt" - "net/http" - - "github.com/juju/errors" jc "github.com/juju/testing/checkers" ociCore "github.com/oracle/oci-go-sdk/v65/core" - ociIdentity "github.com/oracle/oci-go-sdk/v65/identity" - "go.uber.org/mock/gomock" gc "gopkg.in/check.v1" "github.com/juju/juju/core/constraints" - "github.com/juju/juju/core/instance" - "github.com/juju/juju/environs" - envcontext "github.com/juju/juju/environs/context" - "github.com/juju/juju/environs/tags" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/provider/oci" - "github.com/juju/juju/testing" + "github.com/juju/juju/environs/instances" ) type environSuite struct { - commonSuite - - listInstancesResponse []ociCore.Instance } var _ = gc.Suite(&environSuite{}) -func (s *environSuite) SetUpTest(c *gc.C) { - s.commonSuite.SetUpTest(c) - *oci.MaxPollIterations = 2 - s.listInstancesResponse = - []ociCore.Instance{ - { - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeInstance1"), - LifecycleState: ociCore.InstanceLifecycleStateRunning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("fakeName"), - FreeformTags: s.tags, - }, - { - AvailabilityDomain: makeStringPointer("fakeZone2"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeInstance2"), - LifecycleState: ociCore.InstanceLifecycleStateRunning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("fakeName2"), - FreeformTags: s.tags, - }, - } - -} - -func (s *environSuite) setupAvailabilityDomainsExpectations(times int) { - request, response := makeListAvailabilityDomainsRequestResponse([]ociIdentity.AvailabilityDomain{ - { - Name: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - }, - { - Name: makeStringPointer("fakeZone2"), - CompartmentId: &s.testCompartment, - }, - { - Name: makeStringPointer("fakeZone3"), - CompartmentId: &s.testCompartment, - }, - }) - - expect := s.ident.EXPECT().ListAvailabilityDomains(context.Background(), request).Return(response, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) +func (s *environSuite) TestEnsureShapeConfig(c *gc.C) { + type test struct { + name string + maxCpuCores, maxMem *uint64 + cpuCores, mem uint64 + constraints string + want *ociCore.LaunchInstanceShapeConfigDetails } -} -func makeVcnName(controllerUUID, modelUUID string) string { - return fmt.Sprintf("%s-%s-%s", oci.VcnNamePrefix, controllerUUID, modelUUID) -} - -func (s *environSuite) setupVcnExpectations(vcnId string, t map[string]string, times int) { - vcnName := makeVcnName(t[tags.JujuController], t[tags.JujuModel]) - vcnResponse := []ociCore.Vcn{ + tests := []test{ { - CompartmentId: &s.testCompartment, - CidrBlock: makeStringPointer(oci.DefaultAddressSpace), - Id: &vcnId, - LifecycleState: ociCore.VcnLifecycleStateAvailable, - DefaultRouteTableId: makeStringPointer("fakeRouteTable"), - DefaultSecurityListId: makeStringPointer("fakeSeclist"), - DisplayName: &vcnName, - FreeformTags: s.tags, + name: "not flexible shape, no constraints", + cpuCores: 1, + mem: 1024, + want: nil, }, - } - - expect := s.netw.EXPECT().ListVcns(context.Background(), &s.testCompartment).Return(vcnResponse, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } -} - -func (s *environSuite) setupSecurityListExpectations(vcnId string, t map[string]string, times int) { - name := fmt.Sprintf("juju-seclist-%s-%s", t[tags.JujuController], t[tags.JujuModel]) - request, response := makeListSecurityListsRequestResponse([]ociCore.SecurityList{ { - CompartmentId: &s.testCompartment, - VcnId: &vcnId, - Id: makeStringPointer("fakeSecList"), - DisplayName: &name, - FreeformTags: t, - EgressSecurityRules: []ociCore.EgressSecurityRule{ - { - Destination: makeStringPointer(oci.AllowAllPrefix), - Protocol: makeStringPointer(oci.AllProtocols), - }, - }, - IngressSecurityRules: []ociCore.IngressSecurityRule{ - { - Source: makeStringPointer(oci.AllowAllPrefix), - Protocol: makeStringPointer(oci.AllProtocols), - }, + name: "flexible shape, no constraints => default minimum cpus", + maxCpuCores: makeUint64Pointer(32), + maxMem: makeUint64Pointer(512 * 1024), + cpuCores: 1, + mem: 1024, + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(float32(instances.MinCpuCores)), }, }, - }) - expect := s.fw.EXPECT().ListSecurityLists(context.Background(), request.CompartmentId, &vcnId).Return(response.Items, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } -} - -func (s *environSuite) setupInternetGatewaysExpectations(vcnId string, t map[string]string, times int) { - name := fmt.Sprintf("%s-%s", oci.InternetGatewayPrefix, t[tags.JujuController]) - enabled := true - request, response := makeListInternetGatewaysRequestResponse([]ociCore.InternetGateway{ - { - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeGwId"), - VcnId: &vcnId, - DisplayName: &name, - IsEnabled: &enabled, - }, - }) - expect := s.netw.EXPECT().ListInternetGateways(context.Background(), request.CompartmentId, request.VcnId).Return(response.Items, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } -} - -func (s *environSuite) setupListRouteTableExpectations(vcnId string, t map[string]string, times int) { - name := fmt.Sprintf("%s-%s", oci.RouteTablePrefix, t[tags.JujuController]) - request, response := makeListRouteTableRequestResponse([]ociCore.RouteTable{ - { - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeRouteTableId"), - VcnId: &vcnId, - DisplayName: &name, - FreeformTags: t, - LifecycleState: ociCore.RouteTableLifecycleStateAvailable, - }, - }) - expect := s.netw.EXPECT().ListRouteTables(context.Background(), request.CompartmentId, request.VcnId).Return(response.Items, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } -} - -func (s *environSuite) setupListSubnetsExpectations(vcnId, route string, t map[string]string, times int) { - zone1 := "fakeZone1" - zone2 := "fakeZone2" - zone3 := "fakeZone3" - displayNameZone1 := fmt.Sprintf("juju-%s-%s-%s", zone1, t[tags.JujuController], t[tags.JujuModel]) - displayNameZone2 := fmt.Sprintf("juju-%s-%s-%s", zone2, t[tags.JujuController], t[tags.JujuModel]) - displayNameZone3 := fmt.Sprintf("juju-%s-%s-%s", zone3, t[tags.JujuController], t[tags.JujuModel]) - response := []ociCore.Subnet{ - { - AvailabilityDomain: &zone1, - CidrBlock: makeStringPointer(oci.DefaultAddressSpace), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeSubnetId1"), - VcnId: &vcnId, - DisplayName: &displayNameZone1, - RouteTableId: &route, - LifecycleState: ociCore.SubnetLifecycleStateAvailable, - FreeformTags: t, - }, - { - AvailabilityDomain: &zone2, - CidrBlock: makeStringPointer(oci.DefaultAddressSpace), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeSubnetId2"), - VcnId: &vcnId, - DisplayName: &displayNameZone2, - RouteTableId: &route, - LifecycleState: ociCore.SubnetLifecycleStateAvailable, - FreeformTags: t, - }, - { - AvailabilityDomain: &zone3, - CidrBlock: makeStringPointer(oci.DefaultAddressSpace), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeSubnetId3"), - VcnId: &vcnId, - DisplayName: &displayNameZone3, - RouteTableId: &route, - LifecycleState: ociCore.SubnetLifecycleStateAvailable, - FreeformTags: t, - }, - } - - expect := s.netw.EXPECT().ListSubnets(context.Background(), &s.testCompartment, &vcnId).Return(response, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } -} - -func (s *environSuite) setupListImagesExpectations() { - response := []ociCore.Image{ - { - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeUbuntu1"), - OperatingSystem: makeStringPointer("Canonical Ubuntu"), - OperatingSystemVersion: makeStringPointer("22.04"), - DisplayName: makeStringPointer("Canonical-Ubuntu-22.04-2018.01.11-0"), - }, { - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeUbuntu2"), - OperatingSystem: makeStringPointer("Canonical Ubuntu"), - OperatingSystemVersion: makeStringPointer("22.04"), - DisplayName: makeStringPointer("Canonical-Ubuntu-22.04-2018.01.12-0"), - }, - { - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeCentOS"), - OperatingSystem: makeStringPointer("CentOS"), - OperatingSystemVersion: makeStringPointer("7"), - DisplayName: makeStringPointer("CentOS-7-2017.10.19-0"), - }, - } - s.compute.EXPECT().ListImages(context.Background(), &s.testCompartment).Return(response, nil) - s.compute.EXPECT().ListShapes(context.Background(), gomock.Any(), gomock.Any()).Return(listShapesResponse(), nil).AnyTimes() -} - -func (s *environSuite) TestAvailabilityZones(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupAvailabilityDomainsExpectations(1) - - az, err := s.env.AvailabilityZones(nil) - c.Assert(err, gc.IsNil) - c.Check(len(az), gc.Equals, 3) -} - -func (s *environSuite) TestInstanceAvailabilityZoneNames(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil).Times(2) - - id := instance.Id("fakeInstance1") - req := []instance.Id{ - id, - } - zones, err := s.env.InstanceAvailabilityZoneNames(nil, req) - c.Assert(err, gc.IsNil) - c.Check(len(zones), gc.Equals, 1) - c.Assert(zones[id], gc.Equals, "fakeZone1") - - req = []instance.Id{ - instance.Id("fakeInstance1"), - instance.Id("fakeInstance3"), - } - zones, err = s.env.InstanceAvailabilityZoneNames(nil, req) - c.Assert(err, gc.ErrorMatches, "only some instances were found") - c.Check(len(zones), gc.Equals, 1) -} - -func (s *environSuite) TestInstances(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil).Times(2) - - req := []instance.Id{ - instance.Id("fakeInstance1"), - } - - inst, err := s.env.Instances(nil, req) - c.Assert(err, gc.IsNil) - c.Assert(len(inst), gc.Equals, 1) - c.Assert(inst[0].Id(), gc.Equals, instance.Id("fakeInstance1")) - - req = []instance.Id{ - instance.Id("fakeInstance1"), - instance.Id("fakeInstance3"), - } - inst, err = s.env.Instances(nil, req) - c.Assert(err, gc.ErrorMatches, "only some instances were found") - c.Check(len(inst), gc.Equals, 1) - c.Assert(inst[0].Id(), gc.Equals, instance.Id("fakeInstance1")) -} - -func (s *environSuite) TestPrepareForBootstrap(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupAvailabilityDomainsExpectations(1) - s.ident.EXPECT().ListAvailabilityDomains( - gomock.Any(), gomock.Any()).Return( - ociIdentity.ListAvailabilityDomainsResponse{}, errors.New("got error")) - - ctx := envtesting.BootstrapTODOContext(c) - err := s.env.PrepareForBootstrap(ctx, "controller-1") - c.Assert(err, gc.IsNil) - - err = s.env.PrepareForBootstrap(ctx, "controller-1") - c.Assert(err, gc.ErrorMatches, "got error") -} - -func (s *environSuite) TestCreate(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupAvailabilityDomainsExpectations(1) - s.ident.EXPECT().ListAvailabilityDomains( - gomock.Any(), gomock.Any()).Return( - ociIdentity.ListAvailabilityDomainsResponse{}, errors.New("got error")) - - err := s.env.Create(nil, environs.CreateParams{}) - c.Assert(err, gc.IsNil) - - err = s.env.Create(nil, environs.CreateParams{}) - c.Assert(err, gc.ErrorMatches, "got error") -} - -func (s *environSuite) TestConstraintsValidator(c *gc.C) { - validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) - c.Assert(err, jc.ErrorIsNil) - - cons := constraints.MustParse("arch=amd64") - unsupported, err := validator.Validate(cons) - c.Assert(err, jc.ErrorIsNil) - - c.Check(unsupported, gc.HasLen, 0) - -} - -func (s *environSuite) TestConstraintsValidatorEmpty(c *gc.C) { - validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) - c.Assert(err, jc.ErrorIsNil) - - unsupported, err := validator.Validate(constraints.Value{}) - c.Assert(err, jc.ErrorIsNil) - - c.Check(unsupported, gc.HasLen, 0) -} - -func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) { - validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) - c.Assert(err, jc.ErrorIsNil) - - cons := constraints.MustParse("arch=amd64 tags=foo virt-type=kvm") - unsupported, err := validator.Validate(cons) - c.Assert(err, jc.ErrorIsNil) - - c.Check(unsupported, jc.SameContents, []string{"tags", "virt-type"}) -} - -func (s *environSuite) TestConstraintsValidatorWrongArch(c *gc.C) { - validator, err := s.env.ConstraintsValidator(envcontext.NewEmptyCloudCallContext()) - c.Assert(err, jc.ErrorIsNil) - - cons := constraints.MustParse("arch=ppc64el") - _, err = validator.Validate(cons) - c.Check(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are:.*") -} - -func (s *environSuite) TestControllerInstancesNoControllerInstances(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil) - - ids, err := s.env.ControllerInstances(nil, s.controllerUUID) - c.Assert(err, gc.IsNil) - c.Check(len(ids), gc.Equals, 0) -} - -func (s *environSuite) TestControllerInstancesOneController(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.listInstancesResponse[0].FreeformTags = s.ctrlTags - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil) - - ids, err := s.env.ControllerInstances(nil, s.controllerUUID) - c.Assert(err, gc.IsNil) - c.Check(len(ids), gc.Equals, 1) -} - -func (s *environSuite) TestCloudInit(c *gc.C) { - cfg, err := oci.GetCloudInitConfig(s.env, "ubuntu", 1234, 4321) - c.Assert(err, jc.ErrorIsNil) - script, err := cfg.RenderScript() - c.Assert(err, jc.ErrorIsNil) - c.Check(script, jc.Contains, "/sbin/iptables -I INPUT -p tcp --dport 1234 -j ACCEPT") - c.Check(script, jc.Contains, "/sbin/iptables -I INPUT -p tcp --dport 4321 -j ACCEPT") - c.Check(script, jc.Contains, "/etc/init.d/netfilter-persistent save") - - cfg, err = oci.GetCloudInitConfig(s.env, "ubuntu", 0, 0) - c.Assert(err, jc.ErrorIsNil) - script, err = cfg.RenderScript() - c.Assert(err, jc.ErrorIsNil) - c.Check(script, gc.Not(jc.Contains), "/sbin/iptables -I INPUT -p tcp --dport 1234 -j ACCEPT") - c.Check(script, gc.Not(jc.Contains), "/sbin/iptables -I INPUT -p tcp --dport 4321 -j ACCEPT") - c.Check(script, gc.Not(jc.Contains), "/etc/init.d/netfilter-persistent save") - - cfg, err = oci.GetCloudInitConfig(s.env, "centos", 1234, 4321) - c.Assert(err, jc.ErrorIsNil) - script, err = cfg.RenderScript() - c.Assert(err, jc.ErrorIsNil) - c.Check(script, jc.Contains, "firewall-cmd --zone=public --add-port=1234/tcp --permanent") - c.Check(script, jc.Contains, "firewall-cmd --zone=public --add-port=4321/tcp --permanent") - c.Check(script, jc.Contains, "firewall-cmd --reload") -} - -type instanceTermination struct { - instanceId string - err error -} - -type ociInstanceTermination struct { - instance ociCore.Instance - err error -} - -func (s *environSuite) setupStopInstanceExpectations(instancesDetails []instanceTermination) { - exp := s.compute.EXPECT() - instancesListWithError := []ociInstanceTermination{} - instancesList := []ociCore.Instance{} - - for _, inst := range instancesDetails { - ociInstance := ociCore.Instance{ - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer(inst.instanceId), - LifecycleState: ociCore.InstanceLifecycleStateRunning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("fakeName"), - FreeformTags: s.tags, - } - instancesListWithError = append( - instancesListWithError, - ociInstanceTermination{ - instance: ociInstance, - err: inst.err}) - instancesList = append(instancesList, ociInstance) - } - - _, listInstancesResponse := makeListInstancesRequestResponse(instancesList) - - listInstancesResponse.RawResponse = &http.Response{ - StatusCode: 200, - } - - listCall := exp.ListInstances( - context.Background(), &s.testCompartment).Return( - listInstancesResponse.Items, nil).AnyTimes() - - for _, inst := range instancesListWithError { - requestMachine, responseMachine := makeGetInstanceRequestResponse(inst.instance) - - responseMachine.RawResponse = &http.Response{ - StatusCode: 200, - } - - terminateRequestMachine := ociCore.TerminateInstanceRequest{ - InstanceId: inst.instance.Id, - } - - terminateResponse := ociCore.TerminateInstanceResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - - terminatingInst := inst.instance - terminatingInst.LifecycleState = ociCore.InstanceLifecycleStateTerminating - requestMachineTerminating, responseMachineTerminating := makeGetInstanceRequestResponse(terminatingInst) - - terminatedInst := inst.instance - terminatedInst.LifecycleState = ociCore.InstanceLifecycleStateTerminated - requestMachineTerminated, responseMachineTerminated := makeGetInstanceRequestResponse(terminatedInst) - - getCall := exp.GetInstance( - context.Background(), requestMachine).Return( - responseMachine, nil).AnyTimes().After(listCall) - - terminateCall := exp.TerminateInstance( - context.Background(), terminateRequestMachine).Return( - terminateResponse, inst.err).After(getCall) - - if inst.err == nil { - terminatingCall := exp.GetInstance( - context.Background(), requestMachineTerminating).Return( - responseMachineTerminating, nil).Times(2).After(terminateCall) - exp.GetInstance( - context.Background(), requestMachineTerminated).Return( - responseMachineTerminated, nil).After(terminatingCall) - } else { - exp.GetInstance( - context.Background(), requestMachine).Return( - responseMachine, nil).AnyTimes().After(terminateCall) - } - } -} - -func (s *environSuite) TestStopInstances(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStopInstanceExpectations( - []instanceTermination{ - { - instanceId: "instance1", - err: nil, - }, - }, - ) - - ids := []instance.Id{ - instance.Id("instance1"), - } - err := s.env.StopInstances(nil, ids...) - c.Assert(err, gc.IsNil) - -} - -func (s *environSuite) TestStopInstancesSingleFail(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStopInstanceExpectations( - []instanceTermination{ - { - instanceId: "fakeInstance1", - err: nil, - }, - { - instanceId: "fakeInstance2", - err: errors.Errorf("I failed to terminate"), - }, - { - instanceId: "fakeInstance3", - err: nil, + name: "flexible shape, only MaxCpuCores, no constraints => default minimum cpus", + maxCpuCores: makeUint64Pointer(32), + cpuCores: 1, + mem: 1024, + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(float32(instances.MinCpuCores)), }, }, - ) - - ids := []instance.Id{ - instance.Id("fakeInstance1"), - instance.Id("fakeInstance2"), - instance.Id("fakeInstance3"), - } - err := s.env.StopInstances(nil, ids...) - c.Assert(err, gc.ErrorMatches, "failed to stop instance fakeInstance2: I failed to terminate") - -} - -func (s *environSuite) TestStopInstancesMultipleFail(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStopInstanceExpectations( - []instanceTermination{ - { - instanceId: "fakeInstance1", - err: nil, - }, - { - instanceId: "fakeInstance2", - err: errors.Errorf("I failed to terminate fakeInstance2"), - }, - { - instanceId: "fakeInstance3", - err: nil, - }, - { - instanceId: "fakeInstance4", - err: errors.Errorf("I failed to terminate fakeInstance4"), - }, - }, - ) - - ids := []instance.Id{ - instance.Id("fakeInstance1"), - instance.Id("fakeInstance2"), - instance.Id("fakeInstance3"), - instance.Id("fakeInstance4"), - } - err := s.env.StopInstances(nil, ids...) - // order in which the instances are returned or fail is not guaranteed - c.Assert(err, gc.ErrorMatches, `failed to stop instances \[fakeInstance[24] fakeInstance[24]\]: \[I failed to terminate fakeInstance[24] I failed to terminate fakeInstance[24]\]`) - -} - -func (s *environSuite) TestStopInstancesTimeoutTransitioningToTerminating(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - listInstancesRequest, listInstancesResponse := makeListInstancesRequestResponse( - []ociCore.Instance{ - { - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeInstance1"), - LifecycleState: ociCore.InstanceLifecycleStateRunning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("fakeName"), - FreeformTags: s.tags, - }, - }, - ) - - requestMachine1, responseMachine1 := makeGetInstanceRequestResponse(ociCore.Instance{ - CompartmentId: listInstancesResponse.Items[0].CompartmentId, - AvailabilityDomain: listInstancesResponse.Items[0].AvailabilityDomain, - Id: listInstancesResponse.Items[0].Id, - Region: listInstancesResponse.Items[0].Region, - Shape: listInstancesResponse.Items[0].Shape, - DisplayName: listInstancesResponse.Items[0].DisplayName, - FreeformTags: listInstancesResponse.Items[0].FreeformTags, - LifecycleState: ociCore.InstanceLifecycleStateRunning, - }) - - //s.listInstancesResponse.RawResponse = &http.Response{ - // StatusCode: 200, - //} - responseMachine1.RawResponse = &http.Response{ - StatusCode: 200, - } - - terminateRequestMachine1 := ociCore.TerminateInstanceRequest{ - InstanceId: listInstancesResponse.Items[0].Id, - } - - terminateResponse := ociCore.TerminateInstanceResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - - gomock.InOrder( - s.compute.EXPECT().ListInstances( - context.Background(), listInstancesRequest.CompartmentId).Return( - listInstancesResponse.Items, nil), - s.compute.EXPECT().GetInstance( - context.Background(), requestMachine1).Return( - responseMachine1, nil), - s.compute.EXPECT().TerminateInstance( - context.Background(), terminateRequestMachine1).Return( - terminateResponse, nil), - s.compute.EXPECT().GetInstance( - context.Background(), requestMachine1).Return( - responseMachine1, nil).Times(3), - ) - - ids := []instance.Id{ - instance.Id("fakeInstance1"), - } - err := s.env.StopInstances(nil, ids...) - c.Check(err, gc.ErrorMatches, ".*Instance still in running state after 2 checks") - -} - -func (s *environSuite) TestStopInstancesTimeoutTransitioningToTerminated(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - listInstancesRequest, listInstancesResponse := makeListInstancesRequestResponse( - []ociCore.Instance{ - { - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeInstance2"), - LifecycleState: ociCore.InstanceLifecycleStateRunning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("fakeName"), - FreeformTags: s.tags, - }, - }, - ) - - requestMachine1, responseMachine1 := makeGetInstanceRequestResponse(ociCore.Instance{ - CompartmentId: listInstancesResponse.Items[0].CompartmentId, - AvailabilityDomain: listInstancesResponse.Items[0].AvailabilityDomain, - Id: listInstancesResponse.Items[0].Id, - Region: listInstancesResponse.Items[0].Region, - Shape: listInstancesResponse.Items[0].Shape, - DisplayName: listInstancesResponse.Items[0].DisplayName, - FreeformTags: listInstancesResponse.Items[0].FreeformTags, - LifecycleState: ociCore.InstanceLifecycleStateRunning, - }) - - listInstancesResponse.RawResponse = &http.Response{ - StatusCode: 200, - } - responseMachine1.RawResponse = &http.Response{ - StatusCode: 200, - } - - terminateRequestMachine1 := ociCore.TerminateInstanceRequest{ - InstanceId: listInstancesResponse.Items[0].Id, - } - - terminateResponse := ociCore.TerminateInstanceResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - - responseMachine1Terminating := responseMachine1 - responseMachine1Terminating.Instance.LifecycleState = ociCore.InstanceLifecycleStateTerminating - - gomock.InOrder( - s.compute.EXPECT().ListInstances( - context.Background(), listInstancesRequest.CompartmentId).Return( - listInstancesResponse.Items, nil), - s.compute.EXPECT().GetInstance( - context.Background(), requestMachine1).Return( - responseMachine1, nil), - s.compute.EXPECT().TerminateInstance( - context.Background(), terminateRequestMachine1).Return( - terminateResponse, nil), - s.compute.EXPECT().GetInstance( - context.Background(), requestMachine1).Return( - responseMachine1Terminating, nil).AnyTimes(), - ) - - ids := []instance.Id{ - instance.Id("fakeInstance2"), - } - err := s.env.StopInstances(nil, ids...) - c.Check(err, gc.ErrorMatches, ".*Timed out waiting for instance to transition from TERMINATING to TERMINATED") - -} - -func (s *environSuite) TestAllRunningInstances(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil) - - ids, err := s.env.AllRunningInstances(nil) - c.Assert(err, gc.IsNil) - c.Check(len(ids), gc.Equals, 2) -} - -func (s *environSuite) TestAllRunningInstancesExtraUnrelatedInstance(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - // This instance does not have the model tags. It should be ignored. - unrelatedInstance := newFakeOCIInstance( - "notRelated", s.testCompartment, ociCore.InstanceLifecycleStateRunning) - s.listInstancesResponse = append( - s.listInstancesResponse, *unrelatedInstance) - - s.compute.EXPECT().ListInstances( - context.Background(), &s.testCompartment).Return( - s.listInstancesResponse, nil) - - ids, err := s.env.AllRunningInstances(nil) - c.Assert(err, gc.IsNil) - c.Check(len(ids), gc.Equals, 2) -} - -func (s *environSuite) setupLaunchInstanceExpectations( - isController bool, tags map[string]string, publicIP bool, launchInstanceMatcher gomock.Matcher, -) { - inst := ociCore.Instance{ - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - Id: makeStringPointer("fakeInstanceId"), - LifecycleState: ociCore.InstanceLifecycleStateProvisioning, - Region: makeStringPointer("us-phoenix-1"), - Shape: makeStringPointer("VM.Standard1.1"), - DisplayName: makeStringPointer("juju-06f00d-0"), - FreeformTags: tags, - } - responseLaunch := ociCore.LaunchInstanceResponse{ - Instance: inst, - } - s.compute.EXPECT().LaunchInstance(context.Background(), launchInstanceMatcher).Return(responseLaunch, nil) - - getInst := inst - if isController { - getInst.LifecycleState = ociCore.InstanceLifecycleStateRunning - - } - getResponse := ociCore.GetInstanceResponse{ - Instance: getInst, - } - s.compute.EXPECT().GetInstance(context.Background(), gomock.Any()).Return(getResponse, nil) - - if isController { - vnicID := "fakeVnicId" - attachRequest, attachResponse := makeListVnicAttachmentsRequestResponse([]ociCore.VnicAttachment{ - { - Id: makeStringPointer("fakeAttachmentId"), - AvailabilityDomain: makeStringPointer("fake"), - CompartmentId: &s.testCompartment, - InstanceId: makeStringPointer("fakeInstanceId"), - LifecycleState: ociCore.VnicAttachmentLifecycleStateAttached, - DisplayName: makeStringPointer("fakeAttachmentName"), - NicIndex: makeIntPointer(0), - VnicId: &vnicID, - }, - }) - - vnicRequest, vnicResponse := makeGetVnicRequestResponse([]ociCore.GetVnicResponse{ - { - Vnic: ociCore.Vnic{ - Id: &vnicID, - PrivateIp: makeStringPointer("10.0.0.20"), - DisplayName: makeStringPointer("fakeVnicName"), - PublicIp: makeStringPointer("2.2.2.2"), - MacAddress: makeStringPointer("aa:aa:aa:aa:aa:aa"), - SubnetId: makeStringPointer("fakeSubnetId"), - LifecycleState: ociCore.VnicLifecycleStateAvailable, - }, - }, - }) - - // These calls are only expected if we assign a public IP. - // They occur when polling for the IP after the instance is started. - if publicIP { - s.compute.EXPECT().ListVnicAttachments(context.Background(), attachRequest.CompartmentId, makeStringPointer("fakeInstanceId")).Return(attachResponse.Items, nil) - s.netw.EXPECT().GetVnic(context.Background(), vnicRequest[0]).Return(vnicResponse[0], nil) - } - } -} - -func (s *environSuite) setupEnsureNetworksExpectations(vcnId string, machineTags map[string]string) { - s.setupAvailabilityDomainsExpectations(0) - s.setupVcnExpectations(vcnId, machineTags, 1) - s.setupSecurityListExpectations(vcnId, machineTags, 1) - s.setupInternetGatewaysExpectations(vcnId, machineTags, 1) - s.setupListRouteTableExpectations(vcnId, machineTags, 1) - s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 1) -} - -func (s *environSuite) setupStartInstanceExpectations( - isController bool, publicIP bool, launchInstanceMatcher gomock.Matcher) { - vcnId := "fakeVCNId" - machineTags := map[string]string{ - tags.JujuController: testing.ControllerTag.Id(), - tags.JujuModel: testing.ModelTag.Id(), - } - - if isController { - machineTags[tags.JujuIsController] = "true" - } - - s.setupEnsureNetworksExpectations(vcnId, machineTags) - s.setupListImagesExpectations() - s.setupLaunchInstanceExpectations(isController, machineTags, publicIP, launchInstanceMatcher) -} - -func (s *environSuite) TestBootstrap(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStartInstanceExpectations(true, true, gomock.Any()) - - ctx := envtesting.BootstrapTODOContext(c) - _, err := s.env.Bootstrap(ctx, nil, - environs.BootstrapParams{ - ControllerConfig: testing.FakeControllerConfig(), - AvailableTools: makeToolsList("ubuntu"), - BootstrapSeries: "jammy", - SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, - }) - c.Assert(err, gc.IsNil) -} - -func (s *environSuite) TestBootstrapFlexibleShape(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStartInstanceExpectations(true, true, gomock.Any()) - - ctx := envtesting.BootstrapTODOContext(c) - _, err := s.env.Bootstrap(ctx, nil, - environs.BootstrapParams{ - ControllerConfig: testing.FakeControllerConfig(), - AvailableTools: makeToolsList("ubuntu"), - BootstrapSeries: "jammy", - SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, - BootstrapConstraints: constraints.MustParse("cpu-cores=16"), - }) - c.Assert(err, gc.IsNil) -} - -type noPublicIPMatcher struct{} - -func (noPublicIPMatcher) Matches(arg interface{}) bool { - li := arg.(ociCore.LaunchInstanceRequest) - assign := *li.CreateVnicDetails.AssignPublicIp - return !assign -} - -func (noPublicIPMatcher) String() string { return "" } - -func (s *environSuite) TestBootstrapNoAllocatePublicIP(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - s.setupStartInstanceExpectations(true, false, noPublicIPMatcher{}) - - ctx := envtesting.BootstrapTODOContext(c) - _, err := s.env.Bootstrap(ctx, nil, - environs.BootstrapParams{ - ControllerConfig: testing.FakeControllerConfig(), - AvailableTools: makeToolsList("ubuntu"), - BootstrapSeries: "jammy", - SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, - BootstrapConstraints: constraints.MustParse("allocate-public-ip=false"), - }) - c.Assert(err, gc.IsNil) -} - -func (s *environSuite) TestBootstrapNoMatchingTools(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - vcnId := "fakeVCNId" - machineTags := map[string]string{ - tags.JujuController: testing.ControllerTag.Id(), - tags.JujuModel: testing.ModelTag.Id(), - tags.JujuIsController: "true", - } - - s.setupAvailabilityDomainsExpectations(0) - s.setupVcnExpectations(vcnId, machineTags, 0) - s.setupSecurityListExpectations(vcnId, machineTags, 0) - s.setupInternetGatewaysExpectations(vcnId, machineTags, 0) - s.setupListRouteTableExpectations(vcnId, machineTags, 0) - s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 0) - - ctx := envtesting.BootstrapTODOContext(c) - _, err := s.env.Bootstrap(ctx, nil, - environs.BootstrapParams{ - ControllerConfig: testing.FakeControllerConfig(), - AvailableTools: makeToolsList("centos"), - BootstrapSeries: "jammy", - SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, - }) - c.Assert(err, gc.ErrorMatches, "no matching agent binaries available") - -} - -func (s *environSuite) setupDeleteSecurityListExpectations(seclistId string, times int) { - request := ociCore.DeleteSecurityListRequest{ - SecurityListId: makeStringPointer(seclistId), - } - - response := ociCore.DeleteSecurityListResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - - expect := s.fw.EXPECT().DeleteSecurityList(context.Background(), request).Return(response, nil) - if times == 0 { - expect.AnyTimes() - } else { - expect.Times(times) - } - - requestGet := ociCore.GetSecurityListRequest{ - SecurityListId: makeStringPointer("fakeSecList"), - } - - seclist := ociCore.SecurityList{ - Id: makeStringPointer("fakeSecList"), - LifecycleState: ociCore.SecurityListLifecycleStateTerminated, - } - - responseGet := ociCore.GetSecurityListResponse{ - SecurityList: seclist, - } - - s.fw.EXPECT().GetSecurityList(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() - -} - -func (s *environSuite) setupDeleteSubnetExpectations(subnetIds []string) { - for _, id := range subnetIds { - request := ociCore.DeleteSubnetRequest{ - SubnetId: makeStringPointer(id), - } - - response := ociCore.DeleteSubnetResponse{ - RawResponse: &http.Response{ - StatusCode: 201, + { + name: "flexible shape, only MaxMem, no constraints => default minimum cpus", + maxMem: makeUint64Pointer(512 * 1024), + cpuCores: 1, + mem: 1024, + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(float32(instances.MinCpuCores)), }, - } - s.netw.EXPECT().DeleteSubnet(context.Background(), request).Return(response, nil).AnyTimes() - - requestGet := ociCore.GetSubnetRequest{ - SubnetId: makeStringPointer(id), - } - - subnet := ociCore.Subnet{ - Id: makeStringPointer("fakeSecList"), - LifecycleState: ociCore.SubnetLifecycleStateTerminated, - } - - responseGet := ociCore.GetSubnetResponse{ - Subnet: subnet, - } - - s.netw.EXPECT().GetSubnet(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() - } -} - -func (s *environSuite) setupDeleteRouteTableExpectations(vcnId, routeTableId string, t map[string]string) { - s.setupListRouteTableExpectations(vcnId, t, 1) - request := ociCore.DeleteRouteTableRequest{ - RtId: makeStringPointer(routeTableId), - } - - response := ociCore.DeleteRouteTableResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - s.netw.EXPECT().DeleteRouteTable(context.Background(), request).Return(response, nil).AnyTimes() - - requestGet := ociCore.GetRouteTableRequest{ - RtId: makeStringPointer(routeTableId), - } - - rt := ociCore.RouteTable{ - Id: makeStringPointer(routeTableId), - LifecycleState: ociCore.RouteTableLifecycleStateTerminated, - } - - responseGet := ociCore.GetRouteTableResponse{ - RouteTable: rt, - } - - s.netw.EXPECT().GetRouteTable(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() -} - -func (s *environSuite) setupDeleteInternetGatewayExpectations(vcnId, IgId string, t map[string]string) { - s.setupInternetGatewaysExpectations(vcnId, t, 1) - request := ociCore.DeleteInternetGatewayRequest{ - IgId: &IgId, - } - - response := ociCore.DeleteInternetGatewayResponse{ - RawResponse: &http.Response{ - StatusCode: 201, }, - } - s.netw.EXPECT().DeleteInternetGateway(context.Background(), request).Return(response, nil) - - requestGet := ociCore.GetInternetGatewayRequest{ - IgId: &IgId, - } - - ig := ociCore.InternetGateway{ - Id: &IgId, - LifecycleState: ociCore.InternetGatewayLifecycleStateTerminated, - } - - responseGet := ociCore.GetInternetGatewayResponse{ - InternetGateway: ig, - } - - s.netw.EXPECT().GetInternetGateway(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() -} - -func (s *environSuite) setupDeleteVcnExpectations(vcnId string) { - request := ociCore.DeleteVcnRequest{ - VcnId: &vcnId, - } - - response := ociCore.DeleteVcnResponse{ - RawResponse: &http.Response{ - StatusCode: 201, - }, - } - s.netw.EXPECT().DeleteVcn(context.Background(), request).Return(response, nil) - - requestGet := ociCore.GetVcnRequest{ - VcnId: &vcnId, - } - - vcn := ociCore.Vcn{ - Id: &vcnId, - LifecycleState: ociCore.VcnLifecycleStateTerminated, - } - - responseGet := ociCore.GetVcnResponse{ - Vcn: vcn, - } - - s.netw.EXPECT().GetVcn(context.Background(), requestGet).Return(responseGet, nil).AnyTimes() -} - -func (s *environSuite) setupDeleteVolumesExpectations() { - size := int64(50) - volumes := []ociCore.Volume{ { - Id: makeStringPointer("fakeVolumeID1"), - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - DisplayName: makeStringPointer("fakeVolume1"), - LifecycleState: ociCore.VolumeLifecycleStateAvailable, - SizeInGBs: &size, - FreeformTags: map[string]string{ - tags.JujuController: s.controllerUUID, + name: "flexible shape, cpu constraints", + maxCpuCores: makeUint64Pointer(32), + maxMem: makeUint64Pointer(512 * 1024), + cpuCores: 1, + mem: 1024, + constraints: "cores=31", + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(31), }, }, { - Id: makeStringPointer("fakeVolumeID2"), - AvailabilityDomain: makeStringPointer("fakeZone1"), - CompartmentId: &s.testCompartment, - DisplayName: makeStringPointer("fakeVolume2"), - LifecycleState: ociCore.VolumeLifecycleStateAvailable, - SizeInGBs: &size, - FreeformTags: map[string]string{ - tags.JujuController: s.controllerUUID, + name: "flexible shape, mem constraints", + maxCpuCores: makeUint64Pointer(32), + maxMem: makeUint64Pointer(512 * 1024), + cpuCores: 1, + mem: 1024, + constraints: "mem=64", + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(float32(instances.MinCpuCores)), + MemoryInGBs: makeFloat32Pointer(64), }, }, + { + name: "flexible shape, cpu and mem constraints", + maxCpuCores: makeUint64Pointer(32), + maxMem: makeUint64Pointer(512 * 1024), + cpuCores: 1, + mem: 1024, + constraints: "cores=31 mem=64", + want: &ociCore.LaunchInstanceShapeConfigDetails{ + Ocpus: makeFloat32Pointer(31), + MemoryInGBs: makeFloat32Pointer(64), + }, + }, + } + + for _, test := range tests { + c.Logf("test '%s'", test.name) + instanceSpec := instances.InstanceType{ + MaxCpuCores: test.maxCpuCores, + MaxMem: test.maxMem, + CpuCores: test.cpuCores, + Mem: test.mem, + } + cons, err := constraints.Parse(test.constraints) + c.Assert(err, jc.ErrorIsNil) + instanceDetails := ociCore.LaunchInstanceDetails{} + ensureShapeConfig(instanceSpec, cons, &instanceDetails) + c.Check(instanceDetails.ShapeConfig, gc.DeepEquals, test.want) } - - copyVolumes := volumes - copyVolumes[0].LifecycleState = ociCore.VolumeLifecycleStateTerminated - copyVolumes[1].LifecycleState = ociCore.VolumeLifecycleStateTerminated - - listRequest := ociCore.ListVolumesRequest{ - CompartmentId: &s.testCompartment, - } - - listResponse := ociCore.ListVolumesResponse{ - Items: volumes, - } - - requestVolume1 := ociCore.GetVolumeRequest{ - VolumeId: makeStringPointer("fakeVolumeID1"), - } - - requestVolume2 := ociCore.GetVolumeRequest{ - VolumeId: makeStringPointer("fakeVolumeID2"), - } - - responseVolume1 := ociCore.GetVolumeResponse{ - Volume: copyVolumes[0], - } - - responseVolume2 := ociCore.GetVolumeResponse{ - Volume: copyVolumes[1], - } - - s.storage.EXPECT().ListVolumes(context.Background(), listRequest.CompartmentId).Return(listResponse.Items, nil).AnyTimes() - s.storage.EXPECT().GetVolume(context.Background(), requestVolume1).Return(responseVolume1, nil).AnyTimes() - s.storage.EXPECT().GetVolume(context.Background(), requestVolume2).Return(responseVolume2, nil).AnyTimes() } -func (s *environSuite) TestDestroyController(c *gc.C) { - ctrl := s.patchEnv(c) - defer ctrl.Finish() - - machineTags := map[string]string{ - tags.JujuController: testing.ControllerTag.Id(), - tags.JujuModel: testing.ModelTag.Id(), - } - - vcnId := "fakeVCNId" - s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateRunning, 1) - s.setupStopInstanceExpectations( - []instanceTermination{ - { - instanceId: s.testInstanceID, - err: nil, - }, - }, - ) - s.setupListInstancesExpectations(s.testInstanceID, ociCore.InstanceLifecycleStateTerminated, 0) - s.setupVcnExpectations(vcnId, machineTags, 1) - s.setupListSubnetsExpectations(vcnId, "fakeRouteTableId", machineTags, 1) - s.setupSecurityListExpectations(vcnId, machineTags, 1) - s.setupDeleteRouteTableExpectations(vcnId, "fakeRouteTableId", machineTags) - s.setupDeleteSubnetExpectations([]string{"fakeSubnetId1", "fakeSubnetId2", "fakeSubnetId3"}) - s.setupDeleteSecurityListExpectations("fakeSecList", 0) - s.setupDeleteInternetGatewayExpectations(vcnId, "fakeGwId", machineTags) - s.setupDeleteVcnExpectations(vcnId) - s.setupDeleteVolumesExpectations() +func makeUint64Pointer(val uint64) *uint64 { + return &val +} - err := s.env.DestroyController(nil, s.controllerUUID) - c.Assert(err, gc.IsNil) +func makeFloat32Pointer(val float32) *float32 { + return &val } diff --git a/provider/oci/images.go b/provider/oci/images.go index 567d59ab817..e89c1d8723e 100644 --- a/provider/oci/images.go +++ b/provider/oci/images.go @@ -335,12 +335,16 @@ func instanceTypes(cli ComputeClient, compartmentID, imageID *string) ([]instanc // and minimum values. We assign the max memory and cpu cores // values to the instance type in that case. if val.MemoryOptions != nil { - maxMem := uint64(*val.MemoryOptions.MaxInGBs) * 1024 - newType.MaxMem = &maxMem + if val.MemoryOptions.MaxInGBs != nil { + maxMem := uint64(*val.MemoryOptions.MaxInGBs) * 1024 + newType.MaxMem = &maxMem + } } if val.OcpuOptions != nil { - maxCpuCores := uint64(*val.OcpuOptions.Max) - newType.MaxCpuCores = &maxCpuCores + if val.OcpuOptions.Max != nil { + maxCpuCores := uint64(*val.OcpuOptions.Max) + newType.MaxCpuCores = &maxCpuCores + } } types = append(types, newType) } @@ -445,7 +449,6 @@ func refreshImageCache(cli ComputeClient, compartmentID *string) (*ImageCache, e images := map[corebase.Base][]InstanceImage{} for _, val := range items { - logger.Warningf("*** LISTING SHAPES FOR %s", val.String()) instTypes, err := instanceTypes(cli, compartmentID, val.Id) if err != nil { return nil, err diff --git a/provider/oci/images_test.go b/provider/oci/images_integration_test.go similarity index 100% rename from provider/oci/images_test.go rename to provider/oci/images_integration_test.go diff --git a/provider/oci/instance_test.go b/provider/oci/instance_integration_test.go similarity index 100% rename from provider/oci/instance_test.go rename to provider/oci/instance_integration_test.go diff --git a/provider/oci/networking_test.go b/provider/oci/networking_integration_test.go similarity index 100% rename from provider/oci/networking_test.go rename to provider/oci/networking_integration_test.go diff --git a/provider/oci/provider_test.go b/provider/oci/provider_integration_test.go similarity index 100% rename from provider/oci/provider_test.go rename to provider/oci/provider_integration_test.go diff --git a/provider/oci/storage_test.go b/provider/oci/storage_integration_test.go similarity index 100% rename from provider/oci/storage_test.go rename to provider/oci/storage_integration_test.go diff --git a/provider/oci/storage_volumes_test.go b/provider/oci/storage_volumes_integration_test.go similarity index 100% rename from provider/oci/storage_volumes_test.go rename to provider/oci/storage_volumes_integration_test.go From aaff68ab5b2d274212c7568c0efcd9f520a0b3c5 Mon Sep 17 00:00:00 2001 From: Nicolas Vinuesa Date: Mon, 2 Oct 2023 21:43:57 +0200 Subject: [PATCH 3/3] Fix memory value assignment when passing constraints on OCI flex instances --- provider/oci/environ.go | 2 +- provider/oci/environ_test.go | 4 ++-- provider/oci/images.go | 16 ++++++---------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/provider/oci/environ.go b/provider/oci/environ.go index 9d62f9b745e..933dc93348f 100644 --- a/provider/oci/environ.go +++ b/provider/oci/environ.go @@ -678,7 +678,7 @@ func ensureShapeConfig( // VM.Standard.A1.Flex, if we set 2 Ocpus OCI will set 12GB of // memory (default is 6GB per core). if constraints.HasMem() { - mem := float32(*constraints.Mem) + mem := float32(*constraints.Mem / 1024) instanceDetails.ShapeConfig.MemoryInGBs = &mem } } diff --git a/provider/oci/environ_test.go b/provider/oci/environ_test.go index bde27aa20f0..6f0b3f3fe3a 100644 --- a/provider/oci/environ_test.go +++ b/provider/oci/environ_test.go @@ -78,7 +78,7 @@ func (s *environSuite) TestEnsureShapeConfig(c *gc.C) { maxMem: makeUint64Pointer(512 * 1024), cpuCores: 1, mem: 1024, - constraints: "mem=64", + constraints: "mem=64G", want: &ociCore.LaunchInstanceShapeConfigDetails{ Ocpus: makeFloat32Pointer(float32(instances.MinCpuCores)), MemoryInGBs: makeFloat32Pointer(64), @@ -90,7 +90,7 @@ func (s *environSuite) TestEnsureShapeConfig(c *gc.C) { maxMem: makeUint64Pointer(512 * 1024), cpuCores: 1, mem: 1024, - constraints: "cores=31 mem=64", + constraints: "cores=31 mem=64G", want: &ociCore.LaunchInstanceShapeConfigDetails{ Ocpus: makeFloat32Pointer(31), MemoryInGBs: makeFloat32Pointer(64), diff --git a/provider/oci/images.go b/provider/oci/images.go index e89c1d8723e..a36fc5d2b4a 100644 --- a/provider/oci/images.go +++ b/provider/oci/images.go @@ -334,17 +334,13 @@ func instanceTypes(cli ComputeClient, compartmentID, imageID *string) ([]instanc // OcpuOptions will not be nil and they indicate the maximum // and minimum values. We assign the max memory and cpu cores // values to the instance type in that case. - if val.MemoryOptions != nil { - if val.MemoryOptions.MaxInGBs != nil { - maxMem := uint64(*val.MemoryOptions.MaxInGBs) * 1024 - newType.MaxMem = &maxMem - } + if val.MemoryOptions != nil && val.MemoryOptions.MaxInGBs != nil { + maxMem := uint64(*val.MemoryOptions.MaxInGBs) * 1024 + newType.MaxMem = &maxMem } - if val.OcpuOptions != nil { - if val.OcpuOptions.Max != nil { - maxCpuCores := uint64(*val.OcpuOptions.Max) - newType.MaxCpuCores = &maxCpuCores - } + if val.OcpuOptions != nil && val.OcpuOptions.Max != nil { + maxCpuCores := uint64(*val.OcpuOptions.Max) + newType.MaxCpuCores = &maxCpuCores } types = append(types, newType) }