diff --git a/client/ais/v1/ais/ias.go b/client/ais/v1/ais/ias.go new file mode 100644 index 00000000..26907708 --- /dev/null +++ b/client/ais/v1/ais/ias.go @@ -0,0 +1,1221 @@ +package ais + +import ( + "fmt" + "strings" + + "github.com/urfave/cli/v2" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/ais/v1/client" + client2 "github.com/G-Core/gcorelabscloud-go/client/ais/v2/client" + "github.com/G-Core/gcorelabscloud-go/client/flags" + instance_client "github.com/G-Core/gcorelabscloud-go/client/instances/v1/instances" + "github.com/G-Core/gcorelabscloud-go/client/utils" + cmeta "github.com/G-Core/gcorelabscloud-go/client/utils/metadata" + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiflavors" + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiimages" + ai "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/ais" + img_types "github.com/G-Core/gcorelabscloud-go/gcore/image/v1/images/types" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/types" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + "github.com/G-Core/gcorelabscloud-go/gcore/volume/v1/volumes" +) + +var ( + aiInstanceIDText = "instance_id is mandatory argument" + aiClusterIDText = "cluster_id is mandatory argument" + volumeSourceType = types.VolumeSource("").StringList() + volumeType = volumes.VolumeType("").StringList() + interfaceTypes = types.InterfaceType("").StringList() + interfaceFloatingIPSource = types.FloatingIPSource("").StringList() + visibilityTypes = img_types.Visibility("").StringList() + bootableIndex = 0 +) + +var Commands = cli.Command{ + Name: "ai", + Usage: "GCloud AI API", + Subcommands: []*cli.Command{ + &aiClusterGetCommand, + &aiClustersListCommand, + &aiClusterCreateCommand, + &aiClusterDeleteCommand, + &aiClusterPowerCycleCommand, + &aiClusterRebootCommand, + &aiClusterSuspendCommand, + &aiClusterResumeCommand, + &aiClusterResizeCommand, + { + Name: "interface", + Usage: "AI cluster interface action", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiClusterListInterfacesCommand, + &aiClusterAttachInterfacesCommand, + &aiClusterDetachInterfacesCommand, + }, + }, + { + Name: "port", + Usage: "AI cluster port action", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiClusterListPortsCommand, + }, + }, + { + Name: "instance", + Usage: "AI instances action", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiInstancePowerCycleCommand, + &aiInstanceRebootCommand, + &aiInstanceGetConsoleCommand, + }, + }, + { + Name: "securitygroup", + Usage: "AI cluster security groups", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiClusterAssignSecurityGroupsCommand, + &aiClusterUnAssignSecurityGroupsCommand, + }, + }, + { + Name: "image", + Usage: "AI cluster available images", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiClusterAvailableImagesCommand, + }, + }, + { + Name: "flavor", + Usage: "AI cluster available flavors", + Category: "cluster", + Subcommands: []*cli.Command{ + &aiClusterAvailableFlavorsCommand, + }, + }, + { + Name: "metadata", + Usage: "AI cluster metadata", + Category: "AI cluster metadata", + Subcommands: []*cli.Command{ + cmeta.NewMetadataListCommand( + client.NewAIClusterClientV1, + "Get AI cluster metadata", + "", + "cluster_id is mandatory argument", + ), + cmeta.NewMetadataGetCommand( + client2.NewAIClusterClientV2, + "Show AI cluster metadata by key", + "", + "cluster_id is mandatory argument", + ), + cmeta.NewMetadataDeleteCommand( + client2.NewAIClusterClientV2, + "Delete AI cluster metadata by key", + "", + "cluster_id is mandatory argument", + ), + cmeta.NewMetadataCreateCommand( + client2.NewAIClusterClientV2, + "Create AI cluster metadata. It would update existing keys", + "", + "cluster_id is mandatory argument", + ), + cmeta.NewMetadataUpdateCommand( + client2.NewAIClusterClientV2, + "Update AI cluster metadata. It overriding existing records", + "", + "cluster_id is mandatory argument", + ), + cmeta.NewMetadataReplaceCommand( + client2.NewAIClusterClientV2, + "Replace AI cluster metadata. It replace existing records", + "", + "cluster_id is mandatory argument", + ), + }, + }, + }, +} + +var aiClustersListCommand = cli.Command{ + Name: "list", + Usage: "List ai clusters", + Category: "cluster", + Action: func(c *cli.Context) error { + client, err := client2.NewAIClusterClientV2(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + results, err := ai.ListAll(client) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(results, c.String("format")) + return nil + }, +} + +var aiClusterListInterfacesCommand = cli.Command{ + Name: "list", + Usage: "List ai cluster interfaces", + ArgsUsage: "", + Category: "interface", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "list") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + results, err := ai.ListInterfacesAll(client, clusterID) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(results, c.String("format")) + return nil + }, +} + +var aiClusterAttachInterfacesCommand = cli.Command{ + Name: "attach", + Usage: "attach interface to AI instance", + ArgsUsage: "", + Category: "interface", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "interface type", + Required: true, + }, + &cli.StringFlag{ + Name: "network", + Aliases: []string{"n"}, + Usage: "interface network id", + Required: false, + }, + &cli.StringFlag{ + Name: "subnet", + Aliases: []string{"s"}, + Usage: "interface subnet id", + Required: false, + }, + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "interface port id", + Required: false, + }, + &cli.StringFlag{ + Name: "ip-address", + Aliases: []string{"ip"}, + Usage: "interface ip address id", + Required: false, + }, + &cli.StringFlag{ + Name: "floating-type", + Usage: "interface ip address id", + Required: false, + }, + &cli.StringFlag{ + Name: "floating-id", + Aliases: []string{"fip"}, + Usage: "floating ip id for interface", + Required: false, + }, + }, + ), + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiInstanceIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "attach") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + var fipOpts *instances.CreateNewInterfaceFloatingIPOpts + fipSource := types.FloatingIPSource(c.String("floating-type")) + fipID := c.String("floating-id") + if fipSource != "" || fipID != "" { + fipOpts = &instances.CreateNewInterfaceFloatingIPOpts{ + Source: fipSource, + ExistingFloatingID: fipID, + } + } + + opts := ai.AttachInterfaceOpts{ + Type: types.InterfaceType(c.String("type")), + NetworkID: c.String("network"), + SubnetID: c.String("subnet"), + PortID: c.String("port"), + IpAddress: c.String("ip-address"), + FloatingIP: fipOpts, + } + + results, err := ai.AttachAIInstanceInterface(client, instanceID, opts).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(results, c.String("format")) + return nil + }, +} + +var aiClusterDetachInterfacesCommand = cli.Command{ + Name: "detach", + Usage: "detach interface to AI instance", + ArgsUsage: "", + Category: "interface", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "interface port id", + Required: true, + }, + &cli.StringFlag{ + Name: "ip-address", + Aliases: []string{"ip"}, + Usage: "interface ip address id", + Required: true, + }, + }, + ), + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiInstanceIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "attach") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + opts := ai.DetachInterfaceOpts{ + PortID: c.String("port"), + IpAddress: c.String("ip-address"), + } + + results, err := ai.DetachAIInstanceInterface(client, instanceID, opts).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(results, c.String("format")) + return nil + }, +} + +var aiClusterListPortsCommand = cli.Command{ + Name: "list", + Usage: "List ai cluster ports", + ArgsUsage: "", + Category: "port", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "list") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + results, err := ai.ListPortsAll(client, clusterID) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(results, c.String("format")) + return nil + }, +} + +var aiClusterAssignSecurityGroupsCommand = cli.Command{ + Name: "add", + Usage: "Add AI cluster security group", + ArgsUsage: "", + Category: "cluster", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "security group name", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "add") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + opts := instances.SecurityGroupOpts{Name: c.String("name")} + + err = instances.AssignSecurityGroup(client, clusterID, opts).ExtractErr() + if err != nil { + return cli.NewExitError(err, 1) + } + return nil + }, +} + +var aiClusterUnAssignSecurityGroupsCommand = cli.Command{ + Name: "delete", + Usage: "Delete AI cluster security group", + ArgsUsage: "", + Category: "cluster", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "security group name", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "delete") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + opts := instances.SecurityGroupOpts{Name: c.String("name")} + + err = instances.UnAssignSecurityGroup(client, instanceID, opts).ExtractErr() + if err != nil { + return cli.NewExitError(err, 1) + } + return nil + }, +} + +func StringSliceToMetadata(slice []string) (map[string]string, error) { + if len(slice) == 0 { + return nil, nil + } + m := make(map[string]string, len(slice)) + for _, s := range slice { + parts := strings.SplitN(s, "=", 2) + if len(parts) != 2 { + return m, fmt.Errorf("wrong label format: %s", s) + } + m[parts[0]] = parts[1] + } + return m, nil +} + +var aiClusterCreateCommand = cli.Command{ + Name: "create", + Usage: ` + Create AI cluster + Example: gcoreclient ai create --flavor g2a-ai-fake-v1pod-8 --image 256f6681-49bf-449f-8078-6ff6ea771ef4 --keypair sshkey --it external --volume-type standard --volume-source image --volume-image-id 256f6681-49bf-449f-8078-6ff6ea771ef4 --volume-size 20 --name aicluster -d -w`, + Category: "cluster", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "AI cluster name", + Required: true, + }, + &cli.StringFlag{ + Name: "flavor", + Usage: "AI cluster flavor", + Required: true, + }, + &cli.StringFlag{ + Name: "image", + Usage: "AI cluster image", + Required: true, + }, + &cli.StringFlag{ + Name: "keypair", + Aliases: []string{"k"}, + Usage: "AI cluster ssh keypair", + Required: false, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "AI cluster password", + Required: false, + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "AI cluster username", + Required: false, + }, + &cli.StringFlag{ + Name: "user-data", + Usage: "AI cluster user data", + Required: false, + }, + &cli.StringFlag{ + Name: "user-data-file", + Usage: "instance user data file", + Required: false, + }, + &cli.GenericFlag{ + Name: "volume-source", + Aliases: []string{"vs"}, + Value: &utils.EnumStringSliceValue{ + Enum: volumeSourceType, + }, + Usage: fmt.Sprintf("instance volume source. output in %s", strings.Join(volumeSourceType, ", ")), + Required: false, + }, + &cli.IntSliceFlag{ + Name: "volume-boot-index", + Usage: "instance volume boot index", + Required: false, + }, + &cli.IntSliceFlag{ + Name: "volume-size", + Usage: "instance volume size", + Required: false, + }, + &cli.GenericFlag{ + Name: "volume-type", + Aliases: []string{"vt"}, + Value: &utils.EnumStringSliceValue{ + Enum: volumeType, + }, + Usage: fmt.Sprintf("instance volume types. output in %s", strings.Join(volumeType, ", ")), + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-name", + Usage: "instance volume name", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-image-id", + Usage: "instance volume image id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-snapshot-id", + Usage: "instance volume snapshot id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-volume-id", + Usage: "instance volume volume id", + Required: false, + }, + &cli.GenericFlag{ + Name: "interface-type", + Aliases: []string{"it"}, + Value: &utils.EnumStringSliceValue{ + Enum: interfaceTypes, + }, + Usage: fmt.Sprintf("instance interface type. output in %s", strings.Join(interfaceTypes, ", ")), + Required: true, + }, + &cli.StringSliceFlag{ + Name: "interface-network-id", + Usage: "instance interface network id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "interface-subnet-id", + Usage: "instance interface subnet id", + Required: false, + }, + &cli.GenericFlag{ + Name: "interface-floating-source", + Aliases: []string{"ifs"}, + Value: &utils.EnumStringSliceValue{ + Enum: interfaceFloatingIPSource, + }, + Usage: fmt.Sprintf("instance floating ip source. output in %s", strings.Join(interfaceFloatingIPSource, ", ")), + Required: false, + }, + &cli.StringSliceFlag{ + Name: "interface-floating-ip", + Usage: "instance interface existing floating ip. Required when --interface-floating-source set as `existing`", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "security-group", + Usage: "instance security group", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "metadata", + Usage: "instance metadata. Example: --metadata one=two --metadata three=four", + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + userData, err := instance_client.GetUserData(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + instanceVolumes, err := instance_client.GetInstanceVolumes(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + // todo add security group mapping + instanceInterfaces, err := instance_client.GetInterfaces(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + securityGroups := instance_client.GetSecurityGroups(c) + + metadata, err := StringSliceToMetadata(c.StringSlice("metadata")) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + opts := ai.CreateOpts{ + Flavor: c.String("flavor"), + Name: c.String("name"), + ImageID: c.String("image"), + Volumes: instanceVolumes, + Interfaces: instanceInterfaces, + SecurityGroups: securityGroups, + Keypair: c.String("keypair"), + Password: c.String("password"), + Username: c.String("username"), + UserData: userData, + Metadata: metadata, + } + + err = gcorecloud.TranslateValidationError(opts.Validate()) + if err != nil { + return cli.NewExitError(err, 1) + } + + results, err := ai.Create(client, opts).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + + return utils.WaitTaskAndShowResult(c, client, results, true, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + clusterID, err := ai.ExtractAIClusterIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve AI cluster ID from task info: %w", err) + } + cluster, err := ai.Get(client, clusterID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get AI cluster with ID: %s. Error: %w", clusterID, err) + } + return cluster, nil + }) + }, +} + +var aiClusterResizeCommand = cli.Command{ + Name: "resize", + Usage: ` + Resize AI cluster + Example: token ai resize --flavor g2a-ai-fake-v1pod-8 --image 06e62653-1f88-4d38-9aa6-62833e812b4f --keypair sshkey --it any_subnet --interface-network-id 518ba531-496b-4676-8ea4-68e2ed3b2e4b --interface-floating-source new --volume-type standard --volume-source image --volume-image-id 06e62653-1f88-4d38-9aa6-62833e812b4f --volume-size 20 e673bba0-fcef-44d9-904c-824546b608ec -d -w`, + ArgsUsage: "", + Category: "cluster", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "flavor", + Usage: "AI cluster flavor", + Required: true, + }, + &cli.StringFlag{ + Name: "image", + Usage: "AI cluster image", + Required: true, + }, + &cli.StringFlag{ + Name: "keypair", + Aliases: []string{"k"}, + Usage: "AI cluster ssh keypair", + Required: false, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "AI cluster password", + Required: false, + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "AI cluster username", + Required: false, + }, + &cli.StringFlag{ + Name: "user-data", + Usage: "AI cluster user data", + Required: false, + }, + &cli.StringFlag{ + Name: "user-data-file", + Usage: "instance user data file", + Required: false, + }, + &cli.GenericFlag{ + Name: "volume-source", + Aliases: []string{"vs"}, + Value: &utils.EnumStringSliceValue{ + Enum: volumeSourceType, + }, + Usage: fmt.Sprintf("instance volume source. output in %s", strings.Join(volumeSourceType, ", ")), + Required: false, + }, + &cli.IntSliceFlag{ + Name: "volume-boot-index", + Usage: "instance volume boot index", + Required: false, + }, + &cli.IntSliceFlag{ + Name: "volume-size", + Usage: "instance volume size", + Required: false, + }, + &cli.GenericFlag{ + Name: "volume-type", + Aliases: []string{"vt"}, + Value: &utils.EnumStringSliceValue{ + Enum: volumeType, + }, + Usage: fmt.Sprintf("instance volume types. output in %s", strings.Join(volumeType, ", ")), + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-name", + Usage: "instance volume name", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-image-id", + Usage: "instance volume image id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-snapshot-id", + Usage: "instance volume snapshot id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "volume-volume-id", + Usage: "instance volume volume id", + Required: false, + }, + &cli.GenericFlag{ + Name: "interface-type", + Aliases: []string{"it"}, + Value: &utils.EnumStringSliceValue{ + Enum: interfaceTypes, + }, + Usage: fmt.Sprintf("instance interface type. output in %s", strings.Join(interfaceTypes, ", ")), + Required: true, + }, + &cli.StringSliceFlag{ + Name: "interface-network-id", + Usage: "instance interface network id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "interface-subnet-id", + Usage: "instance interface subnet id", + Required: false, + }, + &cli.GenericFlag{ + Name: "interface-floating-source", + Aliases: []string{"ifs"}, + Value: &utils.EnumStringSliceValue{ + Enum: interfaceFloatingIPSource, + }, + Usage: fmt.Sprintf("instance floating ip source. output in %s", strings.Join(interfaceFloatingIPSource, ", ")), + Required: false, + }, + &cli.StringSliceFlag{ + Name: "interface-floating-ip", + Usage: "instance interface existing floating ip. Required when --interface-floating-source set as `existing`", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "security-group", + Usage: "instance security group", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "metadata", + Usage: "instance metadata. Example: --metadata one=two --metadata three=four", + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + userData, err := instance_client.GetUserData(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + instanceVolumes, err := instance_client.GetInstanceVolumes(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + // todo add security group mapping + instanceInterfaces, err := instance_client.GetInterfaces(c) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + securityGroups := instance_client.GetSecurityGroups(c) + + metadata, err := StringSliceToMetadata(c.StringSlice("metadata")) + if err != nil { + _ = cli.ShowCommandHelp(c, "create") + return cli.NewExitError(err, 1) + } + + opts := ai.ResizeAIClusterOpts{ + Flavor: c.String("flavor"), + ImageID: c.String("image"), + Volumes: instanceVolumes, + Interfaces: instanceInterfaces, + SecurityGroups: securityGroups, + Keypair: c.String("keypair"), + Password: c.String("password"), + Username: c.String("username"), + UserData: userData, + Metadata: metadata, + } + + err = gcorecloud.TranslateValidationError(opts.Validate()) + if err != nil { + return cli.NewExitError(err, 1) + } + + results, err := ai.Resize(client, clusterID, opts).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + + return utils.WaitTaskAndShowResult(c, client, results, true, func(task tasks.TaskID) (interface{}, error) { + cluster, err := ai.Get(client, clusterID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get AI cluster with ID: %s. Error: %w", clusterID, err) + } + return cluster, nil + }) + }, +} + +var aiClusterGetCommand = cli.Command{ + Name: "show", + Usage: "Get ai cluster information", + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + client, err := client2.NewAIClusterClientV2(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + instance, err := ai.Get(client, clusterID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instance, c.String("format")) + return nil + }, +} + +var aiClusterDeleteCommand = cli.Command{ + Name: "delete", + Usage: "Delete AI cluster", + Flags: append([]cli.Flag{ + &cli.StringSliceFlag{ + Name: "volume-id", + Usage: "instance volume id", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "floating-ip", + Usage: "delete selected cluster floating ips", + Required: false, + }, + &cli.BoolFlag{ + Name: "delete-floating-ips", + Usage: "delete all instance floating ips", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "reserved-fixed-ip", + Usage: "delete selected instance reserved fixed ips", + Required: false, + }, + }, flags.WaitCommandFlags...), + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + client, err := client2.NewAIClusterClientV2(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + opts := ai.DeleteOpts{ + Volumes: c.StringSlice("volume-id"), + DeleteFloatings: c.Bool("delete-floating-ips"), + FloatingIPs: c.StringSlice("floating-ip"), + ReservedFixedIPs: c.StringSlice("reserved-fixed-ip"), + } + + err = gcorecloud.TranslateValidationError(opts.Validate()) + if err != nil { + return cli.NewExitError(err, 1) + } + + results, err := ai.Delete(client, clusterID, opts).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + + return utils.WaitTaskAndShowResult(c, client, results, false, func(task tasks.TaskID) (interface{}, error) { + _, err := ai.Get(client, clusterID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete AI cluster with ID: %s", clusterID) + } + switch err.(type) { + case gcorecloud.ErrDefault404: + return nil, nil + default: + return nil, err + } + }) + }, +} + +var aiClusterPowerCycleCommand = cli.Command{ + Name: "powercycle", + Usage: "Stop and start AI cluster. Aka hard reboot", + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "powercycle") + return err + } + client, err := client2.NewAIClusterClientV2(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + aiInstances, err := ai.PowerCycleAICluster(client, clusterID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(aiInstances, c.String("format")) + return nil + }, +} + +var aiInstancePowerCycleCommand = cli.Command{ + Name: "powercycle", + Usage: "Stop and start AI instance. Aka hard reboot", + ArgsUsage: "", + Category: "instances", + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiInstanceIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "powercycle") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + instance, err := ai.PowerCycleAIInstance(client, instanceID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instance, c.String("format")) + return nil + }, +} + +var aiClusterRebootCommand = cli.Command{ + Name: "reboot", + Usage: "Reboot AI cluster instaces", + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "reboot") + return err + } + client, err := client2.NewAIClusterClientV2(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + instances, err := ai.RebootAICluster(client, clusterID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instances, c.String("format")) + return nil + }, +} + +var aiInstanceRebootCommand = cli.Command{ + Name: "reboot", + Usage: "Reboot AI instance", + ArgsUsage: "", + Category: "instances", + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiInstanceIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "reboot") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + instance, err := ai.RebootAIInstance(client, instanceID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instance, c.String("format")) + return nil + }, +} + +var aiInstanceGetConsoleCommand = cli.Command{ + Name: "console", + Usage: "Get AI instance console", + ArgsUsage: "", + Category: "instances", + Action: func(c *cli.Context) error { + instanceID, err := flags.GetFirstStringArg(c, aiInstanceIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "console") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + console, err := ai.GetInstanceConsole(client, instanceID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(console, c.String("format")) + return nil + }, +} + +var aiClusterSuspendCommand = cli.Command{ + Name: "suspend", + Usage: "Suspend AI cluster", + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + cluserID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "suspend") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + instance, err := ai.Suspend(client, cluserID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instance, c.String("format")) + return nil + }, +} + +var aiClusterResumeCommand = cli.Command{ + Name: "resume", + Usage: "Resume AI cluser", + ArgsUsage: "", + Category: "cluster", + Action: func(c *cli.Context) error { + clusterID, err := flags.GetFirstStringArg(c, aiClusterIDText) + if err != nil { + _ = cli.ShowCommandHelp(c, "resume") + return err + } + client, err := client.NewAIClusterClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + + instances, err := ai.Resume(client, clusterID).Extract() + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(instances, c.String("format")) + return nil + }, +} + +var aiClusterAvailableImagesCommand = cli.Command{ + Name: "list", + Usage: "List images available for AI cluser", + Category: "image", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "private", + Aliases: []string{"p"}, + Usage: "only private images. any value to show private images", + Required: false, + }, + &cli.StringFlag{ + Name: "visibility", + Aliases: []string{"v"}, + Usage: fmt.Sprintf("image visibility type. output in %s", strings.Join(visibilityTypes, ", ")), + Required: false, + }, + }, + Action: func(c *cli.Context) error { + client, err := client.NewAIImageClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + opts := aiimages.AIImageListOpts{ + Visibility: c.String("visibility"), + Private: c.String("private"), + } + images, err := aiimages.ListAll(client, opts) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(images, c.String("format")) + return nil + }, +} + + +var aiClusterAvailableFlavorsCommand = cli.Command{ + Name: "list", + Usage: "List flavors available for AI cluser", + Category: "flavor", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "disabled", + Usage: "show disabled flavors", + Required: false, + }, + &cli.BoolFlag{ + Name: "capacity", + Usage: "show flavor capacity", + Required: false, + }, + &cli.BoolFlag{ + Name: "price", + Usage: "show flavor price", + Required: false, + }, + + }, + Action: func(c *cli.Context) error { + client, err := client.NewAIFlavorClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.NewExitError(err, 1) + } + opts := aiflavors.AIFlavorListOpts{ + Disabled: c.Bool("disabled"), + IncludeCapacity: c.Bool("capacity"), + IncludePrices: c.Bool("price"), + } + flavors, err := aiflavors.ListAll(client, opts) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.ShowResults(flavors, c.String("format")) + return nil + }, +} \ No newline at end of file diff --git a/client/ais/v1/client/client.go b/client/ais/v1/client/client.go new file mode 100644 index 00000000..8e32e902 --- /dev/null +++ b/client/ais/v1/client/client.go @@ -0,0 +1,23 @@ +package client + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/common" + + "github.com/urfave/cli/v2" +) + +func NewAIClusterClientV1(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "ai/clusters", "v1") +} + +func NewAIImageClientV1(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "ai/images", "v1") +} +func NewAIFlavorClientV1(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "ai/flavors", "v1") +} + + + + diff --git a/client/ais/v2/client/client.go b/client/ais/v2/client/client.go new file mode 100644 index 00000000..6750b7eb --- /dev/null +++ b/client/ais/v2/client/client.go @@ -0,0 +1,12 @@ +package client + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/common" + + "github.com/urfave/cli/v2" +) + +func NewAIClusterClientV2(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "ai/clusters", "v2") +} diff --git a/client/instances/v1/instances/instances.go b/client/instances/v1/instances/instances.go index ad701d86..e27a9641 100644 --- a/client/instances/v1/instances/instances.go +++ b/client/instances/v1/instances/instances.go @@ -109,7 +109,7 @@ func StringSliceToAppConfigSetOpts(slice []string) (map[string]interface{}, erro return m, nil } -func getUserData(c *cli.Context) (string, error) { +func GetUserData(c *cli.Context) (string, error) { userData := "" userDataFile := c.String("user-data-file") userDataContent := c.String("user-data") @@ -126,7 +126,7 @@ func getUserData(c *cli.Context) (string, error) { return userData, nil } -func getInstanceVolumes(c *cli.Context) ([]instances.CreateVolumeOpts, error) { +func GetInstanceVolumes(c *cli.Context) ([]instances.CreateVolumeOpts, error) { volumeSources := utils.GetEnumStringSliceValue(c, "volume-source") volumeTypes := utils.GetEnumStringSliceValue(c, "volume-type") volumeBootIndexes := c.IntSlice("volume-boot-index") @@ -198,7 +198,7 @@ func getInstanceVolumes(c *cli.Context) ([]instances.CreateVolumeOpts, error) { } -func getInterfaces(c *cli.Context) ([]instances.InterfaceInstanceCreateOpts, error) { +func GetInterfaces(c *cli.Context) ([]instances.InterfaceInstanceCreateOpts, error) { interfaceTypes := utils.GetEnumStringSliceValue(c, "interface-type") interfaceNetworkIDs := c.StringSlice("interface-network-id") interfaceSubnetIDs := c.StringSlice("interface-subnet-id") @@ -284,7 +284,7 @@ func getBaremetalInterfaces(c *cli.Context) ([]bminstances.InterfaceOpts, error) } -func getSecurityGroups(c *cli.Context) []gcorecloud.ItemID { +func GetSecurityGroups(c *cli.Context) []gcorecloud.ItemID { securityGroups := c.StringSlice("security-group") res := make([]gcorecloud.ItemID, len(securityGroups)) for i, s := range securityGroups { @@ -681,26 +681,26 @@ var instanceCreateCommandV2 = cli.Command{ return cli.NewExitError(err, 1) } - userData, err := getUserData(c) + userData, err := GetUserData(c) if err != nil { _ = cli.ShowCommandHelp(c, "create") return cli.NewExitError(err, 1) } - instanceVolumes, err := getInstanceVolumes(c) + instanceVolumes, err := GetInstanceVolumes(c) if err != nil { _ = cli.ShowCommandHelp(c, "create") return cli.NewExitError(err, 1) } // todo add security group mapping - instanceInterfaces, err := getInterfaces(c) + instanceInterfaces, err := GetInterfaces(c) if err != nil { _ = cli.ShowCommandHelp(c, "create") return cli.NewExitError(err, 1) } - securityGroups := getSecurityGroups(c) + securityGroups := GetSecurityGroups(c) metadata, err := StringSliceToMetadataSetOpts(c.StringSlice("metadata")) if err != nil { @@ -1290,7 +1290,7 @@ var instanceCreateBaremetalCommand = cli.Command{ return cli.NewExitError(err, 1) } - userData, err := getUserData(c) + userData, err := GetUserData(c) if err != nil { _ = cli.ShowCommandHelp(c, "create_baremetal") return cli.NewExitError(err, 1) diff --git a/client/loadbalancers/v1/listeners/lsiteners.go b/client/loadbalancers/v1/listeners/lsiteners.go index 651af2e3..4a22ebf2 100644 --- a/client/loadbalancers/v1/listeners/lsiteners.go +++ b/client/loadbalancers/v1/listeners/lsiteners.go @@ -120,7 +120,6 @@ var listenerCreateSubCommand = cli.Command{ AllowedCIDRS: c.StringSlice("allowed-cidrs") , } - results, err := listeners.Create(client, opts).Extract() if err != nil { return cli.NewExitError(err, 1) diff --git a/cmd/commands.go b/cmd/commands.go index cd17218a..549b5049 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -8,6 +8,7 @@ import ( "github.com/G-Core/gcorelabscloud-go/client/apitokens/v1/apitokens" "github.com/G-Core/gcorelabscloud-go/client/apptemplates/v1/apptemplates" "github.com/G-Core/gcorelabscloud-go/client/file_shares/v1/file_shares" + "github.com/G-Core/gcorelabscloud-go/client/ais/v1/ais" "github.com/G-Core/gcorelabscloud-go/client/flags" "github.com/G-Core/gcorelabscloud-go/client/flavors/v1/flavors" "github.com/G-Core/gcorelabscloud-go/client/floatingips/v1/floatingips" @@ -75,6 +76,7 @@ var commands = []*cli.Command{ &apptemplates.Commands, &apitokens.Commands, &file_shares.Commands, + &ais.Commands, } type clientCommands struct { diff --git a/gcore/ai/v1/aiflavors/requests.go b/gcore/ai/v1/aiflavors/requests.go new file mode 100644 index 00000000..a3e05a63 --- /dev/null +++ b/gcore/ai/v1/aiflavors/requests.go @@ -0,0 +1,51 @@ +package aiflavors + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/pagination" +) + + +type ListOptsBuilder interface { + ToAIFlavorListQuery() (string, error) +} + +type AIFlavorListOpts struct { + Disabled bool `q:"disabled"` + IncludeCapacity bool `q:"include_capacity"` + IncludePrices bool `q:"include_prices"` +} + +// ToAIFlavorListQuery formats a AIFlavorListOpts into a query string. +func (opts AIFlavorListOpts) ToAIFlavorListQuery() (string, error) { + q, err := gcorecloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), err +} + + +// List retrieves list of AI flavors +func List(c *gcorecloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listAIFlavorsURL(c) + if opts != nil { + query, err := opts.ToAIFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return AIFlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAll retrieves list of all AI flavors +func ListAll(c *gcorecloud.ServiceClient, opts ListOptsBuilder) ([]AIFlavor, error) { + results, err := List(c, opts).AllPages() + if err != nil { + return nil, err + } + return ExtractAIFlavors(results) +} \ No newline at end of file diff --git a/gcore/ai/v1/aiflavors/results.go b/gcore/ai/v1/aiflavors/results.go new file mode 100644 index 00000000..d3cada2b --- /dev/null +++ b/gcore/ai/v1/aiflavors/results.go @@ -0,0 +1,91 @@ +package aiflavors + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/pagination" + "github.com/shopspring/decimal" +) + +type commonResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a instance resource. +func (r commonResult) Extract() (*AIFlavor, error) { + var s AIFlavor + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// AIFlavorPage is the page returned by a pager when traversing over a +// collection of instances. +type AIFlavorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of flavors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r AIFlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FlavorPage struct is empty. +func (r AIFlavorPage) IsEmpty() (bool, error) { + is, err := ExtractAIFlavors(r) + return len(is) == 0, err +} + +// ExtractFlavor accepts a Page struct, specifically a FlavorPage struct, +// and extracts the elements into a slice of Flavor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIFlavors(r pagination.Page) ([]AIFlavor, error) { + var s []AIFlavor + err := ExtractAIFlavorsInto(r, &s) + return s, err +} + +func ExtractAIFlavorsInto(r pagination.Page, v interface{}) error { + return r.(AIFlavorPage).Result.ExtractIntoSlicePtr(v, "results") +} + + +type HardwareDescription struct { + CPU string `json:"cpu,omitempty"` + Disk string `json:"disk,omitempty"` + Network string `json:"network,omitempty"` + RAM string `json:"ram,omitempty"` + Ephemeral string `json:"ephemeral,omitempty"` + GPU string `json:"gpu,omitempty"` + IPU string `json:"ipu,omitempty"` + PoplarCount string `json:"poplar_count,omitempty"` + SGXEPCSize string `json:"sgx_epc_size,omitempty"` +} + +// Flavor represents a flavor structure. +type AIFlavor struct { + FlavorID string `json:"flavor_id"` + FlavorName string `json:"flavor_name"` + Disabled bool `json:"disabled"` + ResourceClass string `json:"resource_class"` + PriceStatus *string `json:"price_status,omitempty"` + CurrencyCode *gcorecloud.Currency `json:"currency_code,omitempty"` + PricePerHour *decimal.Decimal `json:"price_per_hour,omitempty"` + PricePerMonth *decimal.Decimal `json:"price_per_month,omitempty"` + HardwareDescription *HardwareDescription `json:"hardware_description,omitempty"` + RAM *int `json:"ram,omitempty"` + VCPUS *int `json:"vcpus,omitempty"` + Capacity *int `json:"capacity,omitempty"` +} + diff --git a/gcore/ai/v1/aiflavors/testing/docs.go b/gcore/ai/v1/aiflavors/testing/docs.go new file mode 100644 index 00000000..0762b234 --- /dev/null +++ b/gcore/ai/v1/aiflavors/testing/docs.go @@ -0,0 +1 @@ +package testing \ No newline at end of file diff --git a/gcore/ai/v1/aiflavors/testing/fixtures.go b/gcore/ai/v1/aiflavors/testing/fixtures.go new file mode 100644 index 00000000..6d4ea949 --- /dev/null +++ b/gcore/ai/v1/aiflavors/testing/fixtures.go @@ -0,0 +1,39 @@ +package testing + +import ( + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiflavors" +) + +const ListResponse = ` +{ + "count": 1, + "results": [ + { + "resource_class": "bm1-ai-small", + "hardware_description": { + "network": "2x100G", + "ipu": "vPOD-16 (Classic)", + "poplar_count": "2" + }, + "disabled": false, + "flavor_name": "bm1-ai-2xsmall-v1pod-16", + "flavor_id": "bm1-ai-2xsmall-v1pod-16" + } + ] +} +` + +var ( + AIFlavor1 = aiflavors.AIFlavor{ + FlavorID: "bm1-ai-2xsmall-v1pod-16", + FlavorName: "bm1-ai-2xsmall-v1pod-16", + Disabled: false, + ResourceClass: "bm1-ai-small", + HardwareDescription: &aiflavors.HardwareDescription{ + Network: "2x100G", + IPU: "vPOD-16 (Classic)", + PoplarCount: "2", + }, + } + ExpectedAIFlavorSlice = []aiflavors.AIFlavor{AIFlavor1} +) diff --git a/gcore/ai/v1/aiflavors/testing/requests_test.go b/gcore/ai/v1/aiflavors/testing/requests_test.go new file mode 100644 index 00000000..f4cb778a --- /dev/null +++ b/gcore/ai/v1/aiflavors/testing/requests_test.go @@ -0,0 +1,59 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiflavors" + "github.com/G-Core/gcorelabscloud-go/pagination" + th "github.com/G-Core/gcorelabscloud-go/testhelper" + fake "github.com/G-Core/gcorelabscloud-go/testhelper/client" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func prepareListTestURLParams(version string, projectID int, regionID int) string { + return fmt.Sprintf("/%s/ai/flavors/%d/%d", version, projectID, regionID) +} + +func prepareListTestURL() string { + return prepareListTestURLParams("v1", fake.ProjectID, fake.RegionID) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, ListResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/flavors", "v1") + count := 0 + + opts := aiflavors.AIFlavorListOpts{} + + err := aiflavors.List(client, opts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := aiflavors.ExtractAIFlavors(page) + require.NoError(t, err) + ct := actual[0] + require.Equal(t, AIFlavor1, ct) + require.Equal(t, ExpectedAIFlavorSlice, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/gcore/ai/v1/aiflavors/urls.go b/gcore/ai/v1/aiflavors/urls.go new file mode 100644 index 00000000..d9bea423 --- /dev/null +++ b/gcore/ai/v1/aiflavors/urls.go @@ -0,0 +1,9 @@ +package aiflavors + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" +) + +func listAIFlavorsURL(c *gcorecloud.ServiceClient) string { + return c.ServiceURL() +} \ No newline at end of file diff --git a/gcore/ai/v1/aiimages/requests.go b/gcore/ai/v1/aiimages/requests.go new file mode 100644 index 00000000..f3a3454c --- /dev/null +++ b/gcore/ai/v1/aiimages/requests.go @@ -0,0 +1,61 @@ +package aiimages + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/pagination" +) + + +type VisibilityType string + +const ( + PRIVATE VisibilityType = "private" + PUBLIC VisibilityType = "public" + SHARED VisibilityType = "shared" +) + +type ListOptsBuilder interface { + ToAIImageListQuery() (string, error) +} + +type AIImageListOpts struct { + Visibility string `q:"visibility" validate:"omitempty,enum"` + Private string `q:"private" validate:"omitempty"` + MetadataK string `q:"metadata_k" validate:"omitempty"` + MetadataKV map[string]string `q:"metadata_kv" validate:"omitempty"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts AIImageListOpts) ToAIImageListQuery() (string, error) { + q, err := gcorecloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), err +} + + + +// List retrieves list of flavors +func List(c *gcorecloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listAIImagesURL(c) + if opts != nil { + query, err := opts.ToAIImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return AIImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAll retrieves list of flavors +func ListAll(c *gcorecloud.ServiceClient, opts ListOptsBuilder) ([]AIImage, error) { + results, err := List(c, opts).AllPages() + if err != nil { + return nil, err + } + return ExtractAIImages(results) +} \ No newline at end of file diff --git a/gcore/ai/v1/aiimages/results.go b/gcore/ai/v1/aiimages/results.go new file mode 100644 index 00000000..35473a7a --- /dev/null +++ b/gcore/ai/v1/aiimages/results.go @@ -0,0 +1,90 @@ +package aiimages + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/gcore/utils/metadata" + "github.com/G-Core/gcorelabscloud-go/pagination" +) + +type commonResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a instance resource. +func (r commonResult) Extract() (*AIImage, error) { + var s AIImage + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// AIFlavorPage is the page returned by a pager when traversing over a +// collection of instances. +type AIImagePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of flavors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r AIImagePage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FlavorPage struct is empty. +func (r AIImagePage) IsEmpty() (bool, error) { + is, err := ExtractAIImages(r) + return len(is) == 0, err +} + +// ExtractFlavor accepts a Page struct, specifically a FlavorPage struct, +// and extracts the elements into a slice of Flavor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIImages(r pagination.Page) ([]AIImage, error) { + var s []AIImage + err := ExtractAIImagesInto(r, &s) + return s, err +} + +func ExtractAIImagesInto(r pagination.Page, v interface{}) error { + return r.(AIImagePage).Result.ExtractIntoSlicePtr(v, "results") +} + +type AIImage struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Status string `json:"status"` + Visibility string `json:"visibility"` + MinDisk int `json:"min_disk"` + MinRAM int `json:"min_ram"` + OsDistro string `json:"os_distro"` + OsType string `json:"os_type"` + OsVersion string `json:"os_version"` + DisplayOrder int `json:"display_order,omitempty"` + CreatedAt gcorecloud.JSONRFC3339Z `json:"created_at"` + UpdatedAt *gcorecloud.JSONRFC3339Z `json:"updated_at"` + SshKey string `json:"ssh_key,omitempty"` + Size int `json:"size"` + CreatorTaskID *string `json:"creator_task_id,omitempty"` + TaskID *string `json:"task_id"` + Region string `json:"region"` + RegionID int `json:"region_id"` + ProjectID int `json:"project_id"` + DiskFormat string `json:"disk_format"` + IsBaremetal bool `json:"is_baremetal,omitempty"` + HwFirmareType string `json:"hw_firmware_type,omitempty"` + HwMachineType string `json:"hw_machine_type,omitempty"` + Architecture string `json:"architecture,omitempty"` + Metadata []metadata.Metadata `json:"metadata_detailed"` +} diff --git a/gcore/ai/v1/aiimages/testing/docs.go b/gcore/ai/v1/aiimages/testing/docs.go new file mode 100644 index 00000000..0762b234 --- /dev/null +++ b/gcore/ai/v1/aiimages/testing/docs.go @@ -0,0 +1 @@ +package testing \ No newline at end of file diff --git a/gcore/ai/v1/aiimages/testing/fixtures.go b/gcore/ai/v1/aiimages/testing/fixtures.go new file mode 100644 index 00000000..501306da --- /dev/null +++ b/gcore/ai/v1/aiimages/testing/fixtures.go @@ -0,0 +1,67 @@ +package testing + +import ( + "time" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiimages" + "github.com/G-Core/gcorelabscloud-go/gcore/utils/metadata" +) + +const ListResponse = ` +{ + "count": 1, + "results": [ + { + "min_ram": 0, + "task_id": null, + "disk_format": "qcow2", + "os_type": "linux", + "visibility": "public", + "min_disk": 0, + "updated_at": "2022-11-17T11:38:03+0000", + "project_id": 516070, + "region_id": 7, + "region": "ED-10 Preprod", + "architecture": "x86_64", + "name": "ubuntu-18.04-x64-poplar-ironic-1.18.0-3.0.0", + "os_distro": "poplar-ubuntu", + "created_at": "2022-11-17T11:22:55+0000", + "vipu_version": "1.18.0", + "size": 5389418496, + "sdk_version": "3.0.0", + "id": "f6aa6e75-ab88-4c19-889d-79133366cb83", + "metadata_detailed": [], + "status": "active", + "os_version": "18.04", + "metadata": {} + } + ] +} +` + +var ( + ImageCreatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2022-11-17T11:22:55+0000") + ImageUpdatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2022-11-17T11:38:03+0000") + AIImage1 = aiimages.AIImage{ + ID: "f6aa6e75-ab88-4c19-889d-79133366cb83", + Name: "ubuntu-18.04-x64-poplar-ironic-1.18.0-3.0.0", + Status: "active", + Visibility: "public", + MinDisk: 0, + MinRAM: 0, + OsDistro: "poplar-ubuntu", + OsType: "linux", + OsVersion: "18.04", + CreatedAt: gcorecloud.JSONRFC3339Z{Time: ImageCreatedAt}, + UpdatedAt: &gcorecloud.JSONRFC3339Z{Time: ImageUpdatedAt}, + Size: 5389418496, + Region: "ED-10 Preprod", + RegionID: 7, + ProjectID: 516070, + DiskFormat: "qcow2", + Architecture: "x86_64", + Metadata: []metadata.Metadata{}, + } + ExpectedAIImageSlice = []aiimages.AIImage{AIImage1} +) diff --git a/gcore/ai/v1/aiimages/testing/requests_test.go b/gcore/ai/v1/aiimages/testing/requests_test.go new file mode 100644 index 00000000..90b53dc2 --- /dev/null +++ b/gcore/ai/v1/aiimages/testing/requests_test.go @@ -0,0 +1,59 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/aiimages" + "github.com/G-Core/gcorelabscloud-go/pagination" + th "github.com/G-Core/gcorelabscloud-go/testhelper" + fake "github.com/G-Core/gcorelabscloud-go/testhelper/client" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func prepareListTestURLParams(version string, projectID int, regionID int) string { + return fmt.Sprintf("/%s/ai/images/%d/%d", version, projectID, regionID) +} + +func prepareListTestURL() string { + return prepareListTestURLParams("v1", fake.ProjectID, fake.RegionID) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, ListResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/images", "v1") + count := 0 + + opts := aiimages.AIImageListOpts{} + + err := aiimages.List(client, opts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := aiimages.ExtractAIImages(page) + require.NoError(t, err) + ct := actual[0] + require.Equal(t, AIImage1, ct) + require.Equal(t, ExpectedAIImageSlice, actual) + return true, nil + }) + + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/gcore/ai/v1/aiimages/urls.go b/gcore/ai/v1/aiimages/urls.go new file mode 100644 index 00000000..ede7dc7a --- /dev/null +++ b/gcore/ai/v1/aiimages/urls.go @@ -0,0 +1,9 @@ +package aiimages + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" +) + +func listAIImagesURL(c *gcorecloud.ServiceClient) string { + return c.ServiceURL() +} diff --git a/gcore/ai/v1/ais/requests.go b/gcore/ai/v1/ais/requests.go new file mode 100644 index 00000000..b7c5fefd --- /dev/null +++ b/gcore/ai/v1/ais/requests.go @@ -0,0 +1,421 @@ +package ai + +import ( + "net/http" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/types" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + "github.com/G-Core/gcorelabscloud-go/gcore/utils/metadata" + "github.com/G-Core/gcorelabscloud-go/pagination" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the Delete request. +type DeleteOptsBuilder interface { + ToAIClusterDeleteQuery() (string, error) +} + +// DeleteOpts. Set parameters for delete operation +type DeleteOpts struct { + Volumes []string `q:"volumes" validate:"omitempty,dive,uuid4" delimiter:"comma"` + DeleteFloatings bool `q:"delete_floatings" validate:"omitempty,allowed_without=FloatingIPs"` + FloatingIPs []string `q:"floatings" validate:"omitempty,allowed_without=DeleteFloatings,dive,uuid4" delimiter:"comma"` + ReservedFixedIPs []string `q:"reserved_fixed_ips" validate:"omitempty,dive,uuid4" delimiter:"comma"` +} + +// ToIAClusterDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToAIClusterDeleteQuery() (string, error) { + if err := opts.Validate(); err != nil { + return "", err + } + q, err := gcorecloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), err +} + +func (opts *DeleteOpts) Validate() error { + return gcorecloud.ValidateStruct(opts) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToAIClusterCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a AI Cluster. +type CreateOpts struct { + Flavor string `json:"flavor" validate:"required,min=1"` + Name string `json:"name" validate:"required,min=3,max=63"` + ImageID string `json:"image_id" validate:"required,uuid4"` + Interfaces []instances.InterfaceInstanceCreateOpts `json:"interfaces" validate:"required,dive"` + Volumes []instances.CreateVolumeOpts `json:"volumes" validate:"required,dive"` + SecurityGroups []gcorecloud.ItemID `json:"security_groups,omitempty" validate:"omitempty,dive,uuid4"` + Keypair string `json:"keypair_name,omitempty"` + Password string `json:"password" validate:"omitempty,required_with=Username"` + Username string `json:"username" validate:"omitempty,required_with=Password"` + UserData string `json:"user_data,omitempty" validate:"omitempty,base64"` + Metadata map[string]string `json:"metadata,omitempty" validate:"omitempty,dive"` +} + +// Validate +func (opts CreateOpts) Validate() error { + return gcorecloud.ValidateStruct(opts) +} + +// ToAIClusterCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToAIClusterCreateMap() (map[string]interface{}, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + mp, err := gcorecloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + return mp, nil +} + +// ResizeAIClusterOptsBuilder builds parameters or change flavor request. +type ResizeAIClusterOptsBuilder interface { + ToResizeAIClusterActionMap() (map[string]interface{}, error) +} + +// ResizeAIClusterOpts represents options used to resize AI Clsuter. +type ResizeAIClusterOpts struct { + Flavor string `json:"flavor" validate:"omitempty,min=1"` + ImageID string `json:"image_id" validate:"omitempty,uuid4"` + Interfaces []instances.InterfaceInstanceCreateOpts `json:"interfaces" validate:"required,dive"` + Volumes []instances.CreateVolumeOpts `json:"volumes" validate:"omitempty,dive"` + SecurityGroups []gcorecloud.ItemID `json:"security_groups,omitempty" validate:"omitempty,dive,uuid4"` + Keypair string `json:"keypair_name,omitempty"` + Password string `json:"password" validate:"omitempty,required_with=Username"` + Username string `json:"username" validate:"omitempty,required_with=Password"` + UserData string `json:"user_data,omitempty" validate:"omitempty,base64"` + Metadata map[string]string `json:"metadata,omitempty" validate:"omitempty,dive"` +} + +// Validate +func (opts ResizeAIClusterOpts) Validate() error { + return gcorecloud.ValidateStruct(opts) +} + +// ToResizeAIClusterActionMap builds a request body from ResizeAIClusterOpts. +func (opts ResizeAIClusterOpts) ToResizeAIClusterActionMap() (map[string]interface{}, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + mp, err := gcorecloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + return mp, nil +} + +// AttachInterfaceOptsBuilder allows extensions to add parameters to the interface request. +type AttachInterfaceOptsBuilder interface { + ToInterfaceAttachMap() (map[string]interface{}, error) +} + +type AttachInterfaceOpts struct { + Type types.InterfaceType `json:"type,omitempty" validate:"omitempty,enum"` + NetworkID string `json:"network_id,omitempty" validate:"rfe=Type:any_subnet,omitempty,uuid4"` + SubnetID string `json:"subnet_id,omitempty" validate:"rfe=Type:subnet,omitempty,uuid4"` + PortID string `json:"port_id,omitempty" validate:"rfe=Type:reserved_fixed_ip,allowed_without_all=NetworkID SubnetID,omitempty,uuid4"` + IpAddress string `json:"ip_address,omitempty" validate:"allowed_without_all=Type NetworkID SubnetID FloatingIP,omitempty"` + FloatingIP *instances.CreateNewInterfaceFloatingIPOpts `json:"floating_ip,omitempty" validate:"omitempty,dive"` +} + +// Validate +func (opts AttachInterfaceOpts) Validate() error { + return gcorecloud.ValidateStruct(opts) +} + +// ToInterfaceAttachMap builds a request body from AttachInterfaceOpts. +func (opts AttachInterfaceOpts) ToInterfaceAttachMap() (map[string]interface{}, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + return gcorecloud.BuildRequestBody(opts, "") +} + +// DetachInterfaceOptsBuilder allows extensions to add parameters to the interface request. +type DetachInterfaceOptsBuilder interface { + ToInterfaceDetachMap() (map[string]interface{}, error) +} + +type DetachInterfaceOpts struct { + PortID string `json:"port_id" validate:"required,uuid4"` + IpAddress string `json:"ip_address" validate:"required,ip"` +} + +// Validate +func (opts DetachInterfaceOpts) Validate() error { + return gcorecloud.ValidateStruct(opts) +} + +// ToInterfaceDetachMap builds a request body from InterfaceOpts. +func (opts DetachInterfaceOpts) ToInterfaceDetachMap() (map[string]interface{}, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + return gcorecloud.BuildRequestBody(opts, "") +} + +func List(client *gcorecloud.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AIClusterPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAll is a convenience function that returns all AI Clusters +func ListAll(client *gcorecloud.ServiceClient) ([]AICluster, error) { + pages, err := List(client).AllPages() + if err != nil { + return nil, err + } + + all, err := ExtractAIClusters(pages) + if err != nil { + return nil, err + } + + return all, nil +} + +// Get retrieves a specific AI Cluster based on its unique ID. +func Get(client *gcorecloud.ServiceClient, id string) (r GetResult) { + url := getURL(client, id) + _, r.Err = client.Get(url, &r.Body, nil) // nolint + return +} + +// ListInterfaces retrieves network interfaces for AI Cluster +func ListInterfaces(client *gcorecloud.ServiceClient, id string) pagination.Pager { + url := interfacesListURL(client, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AIClusterInterfacePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListInterfacesAll is a convenience function that returns all AI Cluster interfaces. +func ListInterfacesAll(client *gcorecloud.ServiceClient, id string) ([]Interface, error) { + pages, err := ListInterfaces(client, id).AllPages() + if err != nil { + return nil, err + } + + all, err := ExtractAIClusterInterfaces(pages) + if err != nil { + return nil, err + } + + return all, nil + +} + +// ListPorts retrieves ports for AI Cluster +func ListPorts(client *gcorecloud.ServiceClient, id string) pagination.Pager { + url := portsListURL(client, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AIClusterPortsPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListPortsAll is a convenience function that returns all AI Cluster ports. +func ListPortsAll(client *gcorecloud.ServiceClient, id string) ([]AIClusterPort, error) { + pages, err := ListPorts(client, id).AllPages() + if err != nil { + return nil, err + } + all, err := ExtractAIClusterPorts(pages) + if err != nil { + return nil, err + } + return all, nil +} + +// AssignSecurityGroup adds a security groups to the AI Cluster +func AssignSecurityGroup(client *gcorecloud.ServiceClient, id string, opts instances.SecurityGroupOptsBuilder) (r SecurityGroupActionResult) { + b, err := opts.ToSecurityGroupActionMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(addSecurityGroupsURL(client, id), b, nil, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// UnAssignSecurityGroup removes a security groups from the AI Cluster +func UnAssignSecurityGroup(client *gcorecloud.ServiceClient, id string, opts instances.SecurityGroupOptsBuilder) (r SecurityGroupActionResult) { + b, err := opts.ToSecurityGroupActionMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(deleteSecurityGroupsURL(client, id), b, nil, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// AttachInterface adds a interface to the AI instance +func AttachAIInstanceInterface(client *gcorecloud.ServiceClient, instance_id string, opts AttachInterfaceOptsBuilder) (r tasks.Result) { + b, err := opts.ToInterfaceAttachMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(attachAIInstanceInterfaceURL(client, instance_id), b, &r.Body, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// DetachInterface removes a interface from the AI instance +func DetachAIInstanceInterface(client *gcorecloud.ServiceClient, instance_id string, opts DetachInterfaceOptsBuilder) (r tasks.Result) { + b, err := opts.ToInterfaceDetachMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(detachAIInstanceInterfaceURL(client, instance_id), b, &r.Body, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// Create creates an AI Cluster +func Create(client *gcorecloud.ServiceClient, opts CreateOptsBuilder) (r tasks.Result) { + b, err := opts.ToAIClusterCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, nil) // nolint + return +} + +// Delete an AI Cluster +func Delete(client *gcorecloud.ServiceClient, instanceID string, opts DeleteOptsBuilder) (r tasks.Result) { + url := deleteURL(client, instanceID) + if opts != nil { + query, err := opts.ToAIClusterDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + _, r.Err = client.DeleteWithResponse(url, &r.Body, nil) // nolint + return +} + +// PowerCycle AI instance. +func PowerCycleAIInstance(client *gcorecloud.ServiceClient, instance_id string) (r AIInstanceActionResult) { + _, r.Err = client.Post(powerCycleAIInstanceURL(client, instance_id), nil, &r.Body, nil) // nolint + return +} + +// PowerCycle AI Cluster. +func PowerCycleAICluster(client *gcorecloud.ServiceClient, id string) (r AIClusterActionResult) { + _, r.Err = client.Post(powerCycleAIURL(client, id), nil, &r.Body, nil) // nolint + return +} + +// Reboot AI instance. +func RebootAIInstance(client *gcorecloud.ServiceClient, instance_id string) (r AIInstanceActionResult) { + _, r.Err = client.Post(rebootAIInstanceURL(client, instance_id), nil, &r.Body, nil) // nolint + return +} + +// Reboot AI cluster. +func RebootAICluster(client *gcorecloud.ServiceClient, id string) (r AIClusterActionResult) { + _, r.Err = client.Post(rebootAIURL(client, id), nil, &r.Body, nil) // nolint + return +} + +// Suspend AI Cluster. +func Suspend(client *gcorecloud.ServiceClient, id string) (r tasks.Result) { + _, r.Err = client.Post(suspendAIURL(client, id), nil, &r.Body, nil) // nolint + return +} + +// Resume AI Cluster. +func Resume(client *gcorecloud.ServiceClient, id string) (r tasks.Result) { + _, r.Err = client.Post(resumeAIURL(client, id), nil, &r.Body, nil) // nolint + return +} + +// Resize AI Cluster. +func Resize(client *gcorecloud.ServiceClient, id string, opts ResizeAIClusterOptsBuilder) (r tasks.Result) { + b, err := opts.ToResizeAIClusterActionMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(resizeAIURL(client, id), b, &r.Body, nil) // nolint + return +} + +func MetadataList(client *gcorecloud.ServiceClient, id string) pagination.Pager { + url := metadataURL(client, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return MetadataPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func MetadataListAll(client *gcorecloud.ServiceClient, id string) ([]metadata.Metadata, error) { + pages, err := MetadataList(client, id).AllPages() + if err != nil { + return nil, err + } + all, err := ExtractMetadata(pages) + if err != nil { + return nil, err + } + return all, nil +} + +// MetadataCreateOrUpdate creates or update a metadata for an AI. +func MetadataCreateOrUpdate(client *gcorecloud.ServiceClient, id string, opts map[string]interface{}) (r MetadataActionResult) { + _, r.Err = client.Post(metadataURL(client, id), opts, nil, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// MetadataReplace replace a metadata for an AI Cluster. +func MetadataReplace(client *gcorecloud.ServiceClient, id string, opts map[string]interface{}) (r MetadataActionResult) { + _, r.Err = client.Put(metadataURL(client, id), opts, nil, &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// MetadataDelete deletes defined metadata key for a AI Cluster. +func MetadataDelete(client *gcorecloud.ServiceClient, id string, key string) (r MetadataActionResult) { + _, r.Err = client.Delete(metadataItemURL(client, id, key), &gcorecloud.RequestOpts{ // nolint + OkCodes: []int{http.StatusNoContent, http.StatusOK}, + }) + return +} + +// MetadataGet gets defined metadata key for a AI Cluster. +func MetadataGet(client *gcorecloud.ServiceClient, id string, key string) (r MetadataResult) { + url := metadataItemURL(client, id, key) + + _, r.Err = client.Get(url, &r.Body, nil) // nolint + return +} + +// GetInstanceConsole retrieves a specific spice console based on instance unique ID. +func GetInstanceConsole(client *gcorecloud.ServiceClient, id string) (r RemoteConsoleResult) { + url := getAIInstanceConsoleURL(client, id) + _, r.Err = client.Get(url, &r.Body, nil) // nolint + return +} diff --git a/gcore/ai/v1/ais/results.go b/gcore/ai/v1/ais/results.go new file mode 100644 index 00000000..db96fd91 --- /dev/null +++ b/gcore/ai/v1/ais/results.go @@ -0,0 +1,372 @@ +package ai + +import ( + "encoding/json" + "fmt" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + "github.com/G-Core/gcorelabscloud-go/gcore/utils/metadata" + "github.com/G-Core/gcorelabscloud-go/gcore/volume/v1/volumes" + "github.com/G-Core/gcorelabscloud-go/pagination" +) + +type commonResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a AI Cluster resource. +func (r commonResult) Extract() (*AICluster, error) { + var s AICluster + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +type RemoteConsole struct { + URL string `json:"url"` + Type string `json:"type"` + Protocol string `json:"protocol"` +} + +type RemoteConsoleResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a remote console resource. +func (r RemoteConsoleResult) Extract() (*RemoteConsole, error) { + var rc RemoteConsole + err := r.ExtractInto(&rc) + return &rc, err +} + +func (r RemoteConsoleResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "remote_console") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a AI Cluster. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a AI Cluster. +type GetResult struct { + commonResult +} + +type aiInstanceResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a AI instance action resource. +func (r aiInstanceResult) Extract() (*instances.Instance, error) { + var s instances.Instance + err := r.ExtractInto(&s) + return &s, err +} + +func (r aiInstanceResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + + +// AIInstanceActionResult represents the result of an cluster instance operation. Call its Extract +// method to interpret it as a AI Cluster instance actions. +type AIInstanceActionResult struct { + aiInstanceResult +} + +type aiClusterResult struct { + gcorecloud.Result +} + +// Extract is a function that accepts a result and extracts a AI Cluster action resource. +func (r aiClusterResult) Extract() ([]instances.Instance, error) { + var s []instances.Instance + err := r.ExtractInto(&s) + return s, err +} + +func (r aiClusterResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoSlicePtr(v, "results") +} + +// AIClusterActionResult represents the result of an cluster operation. Call its Extract +// method to interpret it as a AI Cluster actions. +type AIClusterActionResult struct { + aiClusterResult +} + +// DeleteResult represents the result of a delete operation +type DeleteResult struct { + commonResult +} + +// MetadataActionResult represents the result of a create, delete or update operation +type MetadataActionResult struct { + gcorecloud.ErrResult +} + +// MetadataResult represents the result of a get operation +type MetadataResult struct { + commonResult +} + +// Extract is a function that accepts a result and extracts a AI Clsuter metadata resource. +func (r MetadataResult) Extract() (*metadata.Metadata, error) { + var s metadata.Metadata + err := r.ExtractInto(&s) + return &s, err +} + +// SecurityGroupActionResult represents the result of a actions operation +type SecurityGroupActionResult struct { + gcorecloud.ErrResult +} + +type PoplarInterfaceSecGrop struct { + PortID string `json:"port_id"` + NetworkID string `json:"network_id"` + SecurityGroups []string `json:"security_groups"` +} + +type AIClusterInterface struct { + Type string `json:"type"` + NetworkID string `json:"network_id"` + SubnetID string `json:"subnet_id"` + PortID string `json:"port_id"` +} + +// AICluster represents a AI Cluster structure. +type AICluster struct { + ClusterID string `json:"cluster_id"` + ClusterName string `json:"cluster_name"` + ClusterStatus string `json:"cluster_status"` + TaskID *string `json:"task_id"` + TaskStatus string `json:"task_status"` + CreatedAt gcorecloud.JSONRFC3339MilliNoZ `json:"instance_created"` + ImageID string `json:"image_id"` + ImageName string `json:"image_name"` + Flavor string `json:"flavor"` + Volumes []volumes.Volume `json:"volumes"` + SecurityGroups []PoplarInterfaceSecGrop `json:"security_groups"` + Interfaces []AIClusterInterface `json:"interfaces"` + KeypairName string `json:"keypair_name"` + UserData string `json:"user_data"` + Username string `json:"username"` + Password string `json:"password"` + PoplarServer []instances.Instance `json:"poplar_servers"` + Metadata map[string]interface{} `json:"cluster_metadata"` + ProjectID int `json:"project_id"` + RegionID int `json:"region_id"` + Region string `json:"region"` +} + +// Interface represents a AI Cluster interface. +type Interface struct { + PortID string `json:"port_id"` + MacAddress gcorecloud.MAC `json:"mac_address"` + NetworkID string `json:"network_id"` + PortSecurityEnabled bool `json:"port_security_enabled"` + IPAssignments []instances.PortIP `json:"ip_assignments"` + NetworkDetails instances.NetworkDetail `json:"network_details"` + FloatingIPDetails []instances.FloatingIP `json:"floatingip_details"` + SubPorts []instances.SubPort `json:"sub_ports"` +} + +type AIClusterPort struct { + ID string `json:"id"` + Name string `json:"name"` + SecurityGroups []gcorecloud.ItemIDName `json:"security_groups"` +} + +// AIClusterPage is the page returned by a pager when traversing over a +// collection of ai clusters. +type AIClusterPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of AI Clusters has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r AIClusterPage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a AIClusterPage struct is empty. +func (r AIClusterPage) IsEmpty() (bool, error) { + is, err := ExtractAIClusters(r) + return len(is) == 0, err +} + +// AIClusterInterfacePage is the page returned by a pager when traversing over a +// collection of cluster interfaces. +type AIClusterInterfacePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of AI Cluster interfaces has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r AIClusterInterfacePage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a AIClusterInterfacePage struct is empty. +func (r AIClusterInterfacePage) IsEmpty() (bool, error) { + is, err := ExtractAIClusterInterfaces(r) + return len(is) == 0, err +} + +// AIClusterPortsPage is the page returned by a pager when traversing over a +// collection of ai cluster ports. +type AIClusterPortsPage struct { + pagination.LinkedPageBase +} +// NextPageURL is invoked when a paginated collection of ai cluster ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r AIClusterPortsPage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a AIClusterPortsPage struct is empty. +func (r AIClusterPortsPage) IsEmpty() (bool, error) { + is, err := ExtractAIClusterPorts(r) + return len(is) == 0, err +} + +// MetadataPage is the page returned by a pager when traversing over a +// collection of AI Cluster metadata objects. +type MetadataPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of AI Cluster metadata objects has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MetadataPage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MetadataPage struct is empty. +func (r MetadataPage) IsEmpty() (bool, error) { + is, err := ExtractMetadata(r) + return len(is) == 0, err +} + +// ExtractAIClusters accepts a Page struct, specifically a AIClustersPage struct, +// and extracts the elements into a slice of AICluster structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIClusters(r pagination.Page) ([]AICluster, error) { + var s []AICluster + err := ExtractAIClustersInto(r, &s) + return s, err +} + +func ExtractAIClustersInto(r pagination.Page, v interface{}) error { + return r.(AIClusterPage).Result.ExtractIntoSlicePtr(v, "results") +} + +// ExtractAIClusterInterfaces accepts a Page struct, specifically a AIClusterInterfacePage struct, +// and extracts the elements into a slice of AI Cluster interface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIClusterInterfaces(r pagination.Page) ([]Interface, error) { + var s []Interface + err := ExtractAIClusterInterfacesInto(r, &s) + return s, err +} + +// ExtractAIClusterInterfacesInto accepts a Page struct, specifically a AIClusterInterfacePage struct, +// and extracts the elements into a slice of AI Cluster interface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIClusterInterfacesInto(r pagination.Page, v interface{}) error { + return r.(AIClusterInterfacePage).Result.ExtractIntoSlicePtr(v, "results") +} + +func ExtractAIClusterPortInto(r pagination.Page, v interface{}) error { + return r.(AIClusterPortsPage).Result.ExtractIntoSlicePtr(v, "results") +} + +// ExtractAIClusterPorts accepts a Page struct, specifically a AIClusterPortsPage struct, +// and extracts the elements into a slice of cluster porst structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractAIClusterPorts(r pagination.Page) ([]AIClusterPort, error) { + var s []AIClusterPort + err := ExtractAIClusterPortInto(r, &s) + return s, err +} + +// ExtractMetadata accepts a Page struct, specifically a MetadataPage struct, +// and extracts the elements into a slice of ai cluster metadata structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMetadata(r pagination.Page) ([]metadata.Metadata, error) { + var s []metadata.Metadata + err := ExtractMetadataInto(r, &s) + return s, err +} + +func ExtractMetadataInto(r pagination.Page, v interface{}) error { + return r.(MetadataPage).Result.ExtractIntoSlicePtr(v, "results") +} + +// UnmarshalJSON - implements Unmarshaler interface +func (i *AICluster) UnmarshalJSON(data []byte) error { + type Alias AICluster + tmp := (*Alias)(i) + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + return nil +} + +type AIClusterTaskResult struct { + AIClusters []string `json:"ai_clusters"` + // etc +} + +func ExtractAIClusterIDFromTask(task *tasks.Task) (string, error) { + var result AIClusterTaskResult + err := gcorecloud.NativeMapToStruct(task.CreatedResources, &result) + if err != nil { + return "", fmt.Errorf("cannot decode AI cluster information in task structure: %w", err) + } + if len(result.AIClusters) == 0 { + return "", fmt.Errorf("cannot decode ai cluster information in task structure: %w", err) + } + return result.AIClusters[0], nil +} diff --git a/gcore/ai/v1/ais/testing/docs.go b/gcore/ai/v1/ais/testing/docs.go new file mode 100644 index 00000000..0762b234 --- /dev/null +++ b/gcore/ai/v1/ais/testing/docs.go @@ -0,0 +1 @@ +package testing \ No newline at end of file diff --git a/gcore/ai/v1/ais/testing/fixtures.go b/gcore/ai/v1/ais/testing/fixtures.go new file mode 100644 index 00000000..0a171c98 --- /dev/null +++ b/gcore/ai/v1/ais/testing/fixtures.go @@ -0,0 +1,1390 @@ +package testing + +import ( + "net" + "time" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + ai "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/ais" + "github.com/G-Core/gcorelabscloud-go/gcore/flavor/v1/flavors" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + "github.com/G-Core/gcorelabscloud-go/gcore/utils/metadata" + "github.com/G-Core/gcorelabscloud-go/gcore/volume/v1/volumes" +) + +const ListResponse = ` +{ + "count": 1, + "results": [ + { + "flavor": "g2a-ai-fake-v1pod-8", + "task_id": "b34d8be3-73b2-402b-92c8-16e944d65f0c", + "volumes": [ + { + "volume_image_metadata": { + "signature_verified": "False", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "vipu_version": "1.18.0", + "build_ts": "1694789419", + "sdk_version": "3.0.0", + "release_version": "1.8.4", + "display_order": "2004300", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "checksum": "dcb3767a59b4c1f0fbc09b439d8bc789", + "container_format": "bare", + "disk_format": "qcow2", + "min_disk": "0", + "min_ram": "0", + "size": "5703401472" + }, + "updated_at": "2023-09-28T15:24:34+0000", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "name": "ivandts_bootvolume", + "created_at": "2023-09-28T15:23:04+0000", + "bootable": true, + "attachments": [ + { + "attached_at": "2023-09-28T15:24:34+0000", + "attachment_id": "a1f35e2b-afae-4caf-9f09-386c136cec45", + "server_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "volume_id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "device": "/dev/vda" + } + ], + "volume_type": "standard", + "size": 20, + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "status": "in-use", + "limiter_stats": { + "MBps_base_limit": 10, + "iops_base_limit": 120, + "MBps_burst_limit": 100, + "iops_burst_limit": 1200 + }, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec" + }, + "metadata_detailed": [ + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + } + ] + } + ], + "creator_task_id": "b34d8be3-73b2-402b-92c8-16e944d65f0c", + "user_data": "#cloud-config\nssh_pwauth: True\nusers:\n - name: kolya\n passwd: $6$rounds=4096$jB/jrhCWrbx65sHb$e5eLHfdJZ/IhiB06N0i/wPepo1fS3Y2o//D7C.jnw66mEqgPUWFuhGAOShC3lYF3eVGJOnEoWZ6N2fRCHj/4W.\n lock-passwd: False\n sudo: ALL=(ALL:ALL) ALL\n", + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "cluster_metadata_detailed": null, + "task_status": "FINISHED", + "created_at": "2023-09-28 15:25:06.115000", + "cluster_name": "ivandts", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_metadata": null, + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "interfaces": [ + { + "type": "any_subnet", + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b" + } + ], + "poplar_servers": [ + { + "flavor": { + "flavor_id": "g2a-ai-fake-v1pod-8", + "os_type": null, + "architecture": null, + "vcpus": 1, + "ram": 2048, + "flavor_name": "g2a-ai-fake-v1pod-8", + "hardware_description": { + "network": "2x100G", + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)" + } + }, + "task_id": null, + "instance_created": "2023-09-28T15:24:32Z", + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "instance_description": null, + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "instance_name": "ivandts", + "vm_state": "active", + "task_state": null, + "addresses": { + "qa-alex-network": [ + { + "addr": "10.10.0.247", + "type": "fixed" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "addr": "10.191.167.5", + "type": "fixed" + } + ] + }, + "status": "ACTIVE", + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] + } + ], + "security_groups": [ + { + "security_groups": [ + "4c74142d-9374-4aa6-b11b-43469b66f746" + ], + "network_id": "bf572176-2d95-4fe0-9de0-f54a5307fbe6", + "port_id": "d7136b4d-c5f3-4d3b-bd86-aeb01942cfc8" + }, + { + "security_groups": [ + "77ae0765-f262-493a-ba32-d9892436ddd0" + ], + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "port_id": "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142" + } + ], + "keypair_name": null, + "cluster_status": "ACTIVE" + } + ] +} +` + +const GetResponse = ` +{ + "flavor": "g2a-ai-fake-v1pod-8", + "task_id": "b34d8be3-73b2-402b-92c8-16e944d65f0c", + "volumes": [ + { + "volume_image_metadata": { + "signature_verified": "False", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "vipu_version": "1.18.0", + "build_ts": "1694789419", + "sdk_version": "3.0.0", + "release_version": "1.8.4", + "display_order": "2004300", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "checksum": "dcb3767a59b4c1f0fbc09b439d8bc789", + "container_format": "bare", + "disk_format": "qcow2", + "min_disk": "0", + "min_ram": "0", + "size": "5703401472" + }, + "updated_at": "2023-09-28T15:24:34+0000", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "name": "ivandts_bootvolume", + "created_at": "2023-09-28T15:23:04+0000", + "bootable": true, + "attachments": [ + { + "attached_at": "2023-09-28T15:24:34+0000", + "attachment_id": "a1f35e2b-afae-4caf-9f09-386c136cec45", + "server_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "volume_id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "device": "/dev/vda" + } + ], + "volume_type": "standard", + "size": 20, + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "status": "in-use", + "limiter_stats": { + "MBps_base_limit": 10, + "iops_base_limit": 120, + "MBps_burst_limit": 100, + "iops_burst_limit": 1200 + }, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec" + }, + "metadata_detailed": [ + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + } + ] + } + ], + "creator_task_id": "b34d8be3-73b2-402b-92c8-16e944d65f0c", + "user_data": "#cloud-config\nssh_pwauth: True\nusers:\n - name: kolya\n passwd: $6$rounds=4096$jB/jrhCWrbx65sHb$e5eLHfdJZ/IhiB06N0i/wPepo1fS3Y2o//D7C.jnw66mEqgPUWFuhGAOShC3lYF3eVGJOnEoWZ6N2fRCHj/4W.\n lock-passwd: False\n sudo: ALL=(ALL:ALL) ALL\n", + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "cluster_metadata_detailed": null, + "task_status": "FINISHED", + "created_at": "2023-09-28 15:25:06.115000", + "cluster_name": "ivandts", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_metadata": null, + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "interfaces": [ + { + "type": "any_subnet", + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b" + } + ], + "poplar_servers": [ + { + "flavor": { + "flavor_id": "g2a-ai-fake-v1pod-8", + "os_type": null, + "architecture": null, + "vcpus": 1, + "ram": 2048, + "flavor_name": "g2a-ai-fake-v1pod-8", + "hardware_description": { + "network": "2x100G", + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)" + } + }, + "task_id": null, + "instance_created": "2023-09-28T15:24:32Z", + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "instance_description": null, + "project_id": 516070, + "region": "ED-10 Preprod", + "region_id": 7, + "instance_name": "ivandts", + "vm_state": "active", + "task_state": null, + "addresses": { + "qa-alex-network": [ + { + "addr": "10.10.0.247", + "type": "fixed" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "addr": "10.191.167.5", + "type": "fixed" + } + ] + }, + "status": "ACTIVE", + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] + } + ], + "security_groups": [ + { + "security_groups": [ + "4c74142d-9374-4aa6-b11b-43469b66f746" + ], + "network_id": "bf572176-2d95-4fe0-9de0-f54a5307fbe6", + "port_id": "d7136b4d-c5f3-4d3b-bd86-aeb01942cfc8" + }, + { + "security_groups": [ + "77ae0765-f262-493a-ba32-d9892436ddd0" + ], + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "port_id": "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142" + } + ], + "keypair_name": null, + "cluster_status": "ACTIVE" +} +` +const ClusterInterfacesResponse = ` +{ + "count": 1, + "results": [ + { + "port_security_enabled": true, + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "ip_assignments": [ + { + "ip_address": "10.10.0.247", + "subnet_id": "8a5d4b01-4d80-4c7e-ba88-96162e3781a4" + } + ], + "network_details": { + "mtu": 1500, + "project_id": null, + "region": null, + "region_id": null, + "updated_at": "2023-09-21T06:24:34+0000", + "subnets": [ + { + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "enable_dhcp": true, + "host_routes": [], + "updated_at": "2023-09-21T06:24:34+0000", + "creator_task_id": "58cb0400-13d9-4539-8e7c-bd5e66edde2c", + "gateway_ip": "10.10.0.1", + "project_id": null, + "region": null, + "region_id": null, + "name": "qa-alex-subnet", + "created_at": "2023-09-21T06:24:34+0000", + "id": "8a5d4b01-4d80-4c7e-ba88-96162e3781a4", + "cidr": "10.10.0.0/24", + "dns_nameservers": [ + "8.8.8.8", + "8.8.4.4" + ], + "ip_version": 4, + "has_router": false, + "metadata": [] + } + ], + "name": "qa-alex-network", + "external": false, + "shared": false, + "segmentation_id": 338, + "created_at": "2023-09-21T06:24:13+0000", + "creator_task_id": "5f4dd40a-158b-49f2-b1c3-8bf764318ab1", + "type": "vxlan", + "id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "metadata": [] + }, + "port_id": "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142", + "floatingip_details": [], + "mac_address": "fa:16:3e:f5:f2:6b" + } + ] +} +` + +const CreateRequest = ` +{ + "flavor": "g2a-ai-fake-v1pod-8", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "interfaces": [ + { + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "type": "any_subnet" + } + ], + "username": "useruser", + "password": "secret", + "name": "ivandts", + "volumes": [ + { + "boot_index": 0, + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "size": 20, + "source": "image", + "type_name": "standard" + } + ] +} +` + +const ResizeRequest = ` +{ + "flavor": "g2a-ai-fake-v1pod-8", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "interfaces": [ + { + "network_id": "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + "type": "any_subnet" + } + ], + "username": "useruser", + "password": "secret", + "volumes": [ + { + "boot_index": 0, + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "size": 20, + "source": "image", + "type_name": "standard" + } + ] +} +` + +const PortsListResponse = ` +{ + "count": 1, + "results": [ + { + "id": "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142", + "name": "port for instance ivandts", + "security_groups": [ + { + "id": "77ae0765-f262-493a-ba32-d9892436ddd0", + "name": "ivandts FE" + } + ] + } + ] +} +` + +const AssignSecurityGroupsRequest = ` +{ + "name": "Test" +} +` + +const UnAssignSecurityGroupsRequest = ` +{ + "name": "Test" +} +` + +const AttachInterfaceRequest = ` +{ + "type": "subnet", + "subnet_id": "9bc36cf6-407c-4a74-bc83-ce3aa3854c3d" +} +` + +const DetachInterfaceRequest = ` +{ + "ip_address": "192.168.0.23", + "port_id": "9bc36cf6-407c-4a74-bc83-ce3aa3854c3d" +} +` + +const AIClusterPowercycleResponse = ` +{ + "count": 1, + "results": [ + { + "region_id": 7, + "region": "ED-10 Preprod", + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "status": "ACTIVE", + "addresses": { + "qa-alex-network": [ + { + "type": "fixed", + "addr": "10.10.0.247" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "type": "fixed", + "addr": "10.191.167.5" + } + ] + }, + "instance_created": "2023-09-28T15:24:32Z", + "instance_description": null, + "vm_state": "active", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "flavor": { + "os_type": null, + "ram": 2048, + "hardware_description": { + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)", + "network": "2x100G" + }, + "vcpus": 1, + "flavor_name": "g2a-ai-fake-v1pod-8", + "architecture": null, + "flavor_id": "g2a-ai-fake-v1pod-8" + }, + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "project_id": 516070, + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "task_state": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "instance_name": "ivandts", + "task_id": null, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] + } + ] +} +` + +const AIInstancePowercycleResponse = ` +{ + "region_id": 7, + "region": "ED-10 Preprod", + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "status": "ACTIVE", + "addresses": { + "qa-alex-network": [ + { + "type": "fixed", + "addr": "10.10.0.247" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "type": "fixed", + "addr": "10.191.167.5" + } + ] + }, + "instance_created": "2023-09-28T15:24:32Z", + "instance_description": null, + "vm_state": "active", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "flavor": { + "os_type": null, + "ram": 2048, + "hardware_description": { + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)", + "network": "2x100G" + }, + "vcpus": 1, + "flavor_name": "g2a-ai-fake-v1pod-8", + "architecture": null, + "flavor_id": "g2a-ai-fake-v1pod-8" + }, + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "project_id": 516070, + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "task_state": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "instance_name": "ivandts", + "task_id": null, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] +} +` + +const AIInstanceRebootResponse = ` +{ + "region_id": 7, + "region": "ED-10 Preprod", + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "status": "ACTIVE", + "addresses": { + "qa-alex-network": [ + { + "type": "fixed", + "addr": "10.10.0.247" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "type": "fixed", + "addr": "10.191.167.5" + } + ] + }, + "instance_created": "2023-09-28T15:24:32Z", + "instance_description": null, + "vm_state": "active", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "flavor": { + "os_type": null, + "ram": 2048, + "hardware_description": { + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)", + "network": "2x100G" + }, + "vcpus": 1, + "flavor_name": "g2a-ai-fake-v1pod-8", + "architecture": null, + "flavor_id": "g2a-ai-fake-v1pod-8" + }, + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "project_id": 516070, + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "task_state": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "instance_name": "ivandts", + "task_id": null, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] +} +` + +const AIClusterRebootResponse = ` +{ + "count": 1, + "results": [ + { + "region_id": 7, + "region": "ED-10 Preprod", + "instance_id": "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + "keypair_name": null, + "status": "ACTIVE", + "addresses": { + "qa-alex-network": [ + { + "type": "fixed", + "addr": "10.10.0.247" + } + ], + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": [ + { + "type": "fixed", + "addr": "10.191.167.5" + } + ] + }, + "instance_created": "2023-09-28T15:24:32Z", + "instance_description": null, + "vm_state": "active", + "creator_task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "flavor": { + "os_type": null, + "ram": 2048, + "hardware_description": { + "cpu": "1 vCPU", + "ram": "2GB RAM", + "ipu": "vPOD-8 (Classic)", + "network": "2x100G" + }, + "vcpus": 1, + "flavor_name": "g2a-ai-fake-v1pod-8", + "architecture": null, + "flavor_id": "g2a-ai-fake-v1pod-8" + }, + "volumes": [ + { + "id": "459bf28d-df63-45d2-a462-6c216e571ddc", + "delete_on_termination": false + } + ], + "project_id": 516070, + "security_groups": [ + { + "name": "default" + }, + { + "name": "ivandts FE" + } + ], + "task_state": null, + "metadata": { + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f" + }, + "instance_name": "ivandts", + "task_id": null, + "metadata_detailed": [ + { + "key": "cluster_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": false + }, + { + "key": "image_id", + "value": "06e62653-1f88-4d38-9aa6-62833e812b4f", + "read_only": true + }, + { + "key": "image_name", + "value": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "read_only": true + }, + { + "key": "os_distro", + "value": "poplar-ubuntu", + "read_only": true + }, + { + "key": "os_type", + "value": "linux", + "read_only": true + }, + { + "key": "os_version", + "value": "20.04", + "read_only": true + }, + { + "key": "poplar_sdk_version", + "value": "3.0.0", + "read_only": false + }, + { + "key": "task_id", + "value": "e673bba0-fcef-44d9-904c-824546b608ec", + "read_only": true + }, + { + "key": "vipu_version", + "value": "1.18.0", + "read_only": false + } + ] + } + ] +} +` + +const CreateResponse = ` +{ + "tasks": [ + "50f53a35-42ed-40c4-82b2-5a37fb3e00bc" + ] +} +` +const DeleteResponse = ` +{ + "tasks": [ + "50f53a35-42ed-40c4-82b2-5a37fb3e00bc" + ] +} +` + +const MetadataListResponse = ` +{ + "count": 2, + "results": [ + { + "key": "cost-center", + "value": "Atlanta", + "read_only": false + }, + { + "key": "data-center", + "value": "A", + "read_only": false + } + ] +} +` + +const MetadataResponse = ` +{ + "key": "cost-center", + "value": "Atlanta", + "read_only": false +} +` + +const MetadataCreateRequest = ` +{ +"test1": "test1", +"test2": "test2" +} +` + +const InstanceConsoleResponse = ` +{ + "remote_console": + { + "url": "https://console-novnc-ed10.cloud.gcorelabs.com/vnc_auto.html?path=token%3Ddf5d4b4f-f78c-421f-9131-b6be2facf9bd", + "type": "novnc", + "protocol": "vnc" + } +} +` + +var ( + ip1 = net.ParseIP("10.10.0.247") + ip2 = net.ParseIP("10.191.167.5") + tm, _ = time.Parse(gcorecloud.RFC3339MilliNoZ, "2023-09-28 15:25:06.115000") + createdTime = gcorecloud.JSONRFC3339MilliNoZ{Time: tm} + volumeCreatedTime, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-28T15:23:04+0000") + volumeUpdatedTime, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-28T15:24:34+0000") + volumeAttachedTime, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-28T15:24:34+0000") + instanceCreatedTime, _ = time.Parse(gcorecloud.RFC3339ZZ, "2023-09-28T15:24:32Z") + taskID = "b34d8be3-73b2-402b-92c8-16e944d65f0c" + creatorTaskID = "e673bba0-fcef-44d9-904c-824546b608ec" + + AICluster1 = ai.AICluster{ + ClusterID: "e673bba0-fcef-44d9-904c-824546b608ec", + ClusterName: "ivandts", + ClusterStatus: "ACTIVE", + TaskID: &taskID, + TaskStatus: "FINISHED", + CreatedAt: createdTime, + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + ImageName: "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + Flavor: "g2a-ai-fake-v1pod-8", + Volumes: []volumes.Volume{ + { + CreatedAt: gcorecloud.JSONRFC3339Z{Time: volumeCreatedTime}, + UpdatedAt: gcorecloud.JSONRFC3339Z{Time: volumeUpdatedTime}, + VolumeType: "standard", + ID: "459bf28d-df63-45d2-a462-6c216e571ddc", + Name: "ivandts_bootvolume", + RegionName: "ED-10 Preprod", + Status: "in-use", + Size: 20, + Bootable: true, + ProjectID: 516070, + RegionID: 7, + Attachments: []volumes.Attachment{ + { + ServerID: "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + AttachmentID: "a1f35e2b-afae-4caf-9f09-386c136cec45", + AttachedAt: gcorecloud.JSONRFC3339Z{Time: volumeAttachedTime}, + VolumeID: "459bf28d-df63-45d2-a462-6c216e571ddc", + Device: "/dev/vda", + }, + }, + Metadata: []metadata.Metadata{ + { + Key: "task_id", + Value: "e673bba0-fcef-44d9-904c-824546b608ec", + ReadOnly: true, + }, + }, + CreatorTaskID: creatorTaskID, + VolumeImageMetadata: volumes.VolumeImageMetadata{ + ContainerFormat: "bare", + MinRAM: "0", + DiskFormat: "qcow2", + ImageName: "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + MinDisk: "0", + Checksum: "dcb3767a59b4c1f0fbc09b439d8bc789", + Size: "5703401472", + }, + }, + }, + SecurityGroups: []ai.PoplarInterfaceSecGrop{ + { + PortID: "d7136b4d-c5f3-4d3b-bd86-aeb01942cfc8", + NetworkID: "bf572176-2d95-4fe0-9de0-f54a5307fbe6", + SecurityGroups: []string{"4c74142d-9374-4aa6-b11b-43469b66f746"}, + }, + { + PortID: "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142", + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + SecurityGroups: []string{"77ae0765-f262-493a-ba32-d9892436ddd0"}, + }, + }, + Interfaces: []ai.AIClusterInterface{ + { + Type: "any_subnet", + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + }, + }, + UserData: "#cloud-config\nssh_pwauth: True\nusers:\n - name: kolya\n passwd: $6$rounds=4096$jB/jrhCWrbx65sHb$e5eLHfdJZ/IhiB06N0i/wPepo1fS3Y2o//D7C.jnw66mEqgPUWFuhGAOShC3lYF3eVGJOnEoWZ6N2fRCHj/4W.\n lock-passwd: False\n sudo: ALL=(ALL:ALL) ALL\n", + PoplarServer: []instances.Instance{ + { + ID: "a2ff6283-09f9-4c2a-a96f-0bedf7b3dd2d", + Name: "ivandts", + CreatedAt: gcorecloud.JSONRFC3339ZZ{Time: instanceCreatedTime}, + Status: "ACTIVE", + VMState: "active", + AvailabilityZone: "nova", + Flavor: flavors.Flavor{ + FlavorID: "g2a-ai-fake-v1pod-8", + FlavorName: "g2a-ai-fake-v1pod-8", + HardwareDescription: &flavors.HardwareDescription{ + CPU: "1 vCPU", + Network: "2x100G", + RAM: "2GB RAM", + IPU: "vPOD-8 (Classic)", + }, + RAM: 2048, + VCPUS: 1, + }, + Metadata: map[string]interface{}{ + "task_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "cluster_id": "e673bba0-fcef-44d9-904c-824546b608ec", + "vipu_version": "1.18.0", + "poplar_sdk_version": "3.0.0", + "os_distro": "poplar-ubuntu", + "os_type": "linux", + "os_version": "20.04", + "image_name": "ubuntu-20.04-x64-poplar-ironic-1.18.0-3.0.0", + "image_id": "06e62653-1f88-4d38-9aa6-62833e812b4f", + }, + Volumes: []instances.InstanceVolume{ + { + ID: "459bf28d-df63-45d2-a462-6c216e571ddc", + DeleteOnTermination: false, + }, + }, + Addresses: map[string][]instances.InstanceAddress{ + "qa-alex-network": { + { + Type: "fixed", + Address: ip1, + }, + }, + "ipu-cluster-rdma-network-e673bba0-fcef-44d9-904c-824546b608ec": { + { + Type: "fixed", + Address: ip2, + }, + }, + }, + SecurityGroups: []gcorecloud.ItemName{ + { + Name: "default", + }, + { + Name: "ivandts FE", + }, + }, + CreatorTaskID: &creatorTaskID, + ProjectID: 516070, + RegionID: 7, + Region: "ED-10 Preprod", + }, + }, + ProjectID: 516070, + RegionID: 7, + Region: "ED-10 Preprod", + } + + PortID = "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142" + PortMac, _ = gcorecloud.ParseMacString("fa:16:3e:f5:f2:6b") + PortIP1 = net.ParseIP("10.10.0.247") + PortNetworkUpdatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-21T06:24:34+0000") + PortNetworkCreatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-21T06:24:13+0000") + PortNetworkSubnet1CreatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-21T06:24:34+0000") + PortNetworkSubnet1UpdatedAt, _ = time.Parse(gcorecloud.RFC3339Z, "2023-09-21T06:24:34+0000") + PortNetworkSubnet1Cidr, _ = gcorecloud.ParseCIDRString("10.10.0.0/24") + SubnetCreatorTaskID = "58cb0400-13d9-4539-8e7c-bd5e66edde2c" + NetworkDetailsCreatorTask = "5f4dd40a-158b-49f2-b1c3-8bf764318ab1" + SecurityGroup1 = gcorecloud.ItemIDName{ + ID: "77ae0765-f262-493a-ba32-d9892436ddd0", + Name: "ivandts FE", + } + AIClusterPort1 = ai.AIClusterPort{ + ID: "f3dcadf8-a4a5-4e5a-af7e-4c5902cd4142", + Name: "port for instance ivandts", + SecurityGroups: ExpectedSecurityGroupsSlice, + } + AIClusterInterface1 = ai.Interface{ + PortID: PortID, + MacAddress: *PortMac, + PortSecurityEnabled: true, + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + IPAssignments: []instances.PortIP{ + { + IPAddress: PortIP1, + SubnetID: "8a5d4b01-4d80-4c7e-ba88-96162e3781a4", + }, + }, + NetworkDetails: instances.NetworkDetail{ + Mtu: 1500, + UpdatedAt: &gcorecloud.JSONRFC3339Z{Time: PortNetworkUpdatedAt}, + CreatedAt: gcorecloud.JSONRFC3339Z{Time: PortNetworkCreatedAt}, + ID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + External: false, + Default: false, + Shared: false, + Name: "qa-alex-network", + CreatorTaskID: &NetworkDetailsCreatorTask, + Subnets: []instances.Subnet{ + { + ID: "8a5d4b01-4d80-4c7e-ba88-96162e3781a4", + Name: "qa-alex-subnet", + IPVersion: gcorecloud.IPv4, + EnableDHCP: true, + Cidr: *PortNetworkSubnet1Cidr, + CreatedAt: gcorecloud.JSONRFC3339Z{Time: PortNetworkSubnet1CreatedAt}, + UpdatedAt: &gcorecloud.JSONRFC3339Z{Time: PortNetworkSubnet1UpdatedAt}, + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + CreatorTaskID: &SubnetCreatorTaskID, + }, + }, + }, + FloatingIPDetails: []instances.FloatingIP{}, + } + ExpectedAIClusterSlice = []ai.AICluster{AICluster1} + ExpectedAIClusterInterfacesSlice = []ai.Interface{AIClusterInterface1} + ExpectedSecurityGroupsSlice = []gcorecloud.ItemIDName{SecurityGroup1} + ExpectedPortsSlice = []ai.AIClusterPort{AIClusterPort1} + + Tasks1 = tasks.TaskResults{ + Tasks: []tasks.TaskID{"50f53a35-42ed-40c4-82b2-5a37fb3e00bc"}, + } + Metadata1 = metadata.Metadata{ + Key: "cost-center", + Value: "Atlanta", + ReadOnly: false, + } + Metadata2 = metadata.Metadata{ + Key: "data-center", + Value: "A", + ReadOnly: false, + } + ExpectedMetadataList = []metadata.Metadata{Metadata1, Metadata2} + Console = ai.RemoteConsole{ + URL: "https://console-novnc-ed10.cloud.gcorelabs.com/vnc_auto.html?path=token%3Ddf5d4b4f-f78c-421f-9131-b6be2facf9bd", + Type: "novnc", + Protocol: "vnc", + } +) diff --git a/gcore/ai/v1/ais/testing/requests_test.go b/gcore/ai/v1/ais/testing/requests_test.go new file mode 100644 index 00000000..520aad16 --- /dev/null +++ b/gcore/ai/v1/ais/testing/requests_test.go @@ -0,0 +1,727 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + ai "github.com/G-Core/gcorelabscloud-go/gcore/ai/v1/ais" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" + "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/types" + "github.com/G-Core/gcorelabscloud-go/gcore/volume/v1/volumes" + th "github.com/G-Core/gcorelabscloud-go/testhelper" + fake "github.com/G-Core/gcorelabscloud-go/testhelper/client" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func prepareListTestURLParams(version string, projectID int, regionID int) string { + return fmt.Sprintf("/%s/ai/clusters/%d/%d", version, projectID, regionID) +} + +func prepareGetTestURLParams(version string, projectID int, regionID int, id string) string { + return fmt.Sprintf("/%s/ai/clusters/%d/%d/%s", version, projectID, regionID, id) +} + +func prepareGetActionTestURLParams(version string, id string, action string) string { // nolint + return fmt.Sprintf("/%s/ai/clusters/%d/%d/%s/%s", version, fake.ProjectID, fake.RegionID, id, action) +} + +func prepareListTestURL() string { + return prepareListTestURLParams("v1", fake.ProjectID, fake.RegionID) +} + +func prepareListInterfacesTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "interfaces") +} + +func prepareGetInstanceConsoleTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "get_console") +} + +func prepareListPortsTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "ports") +} + +func prepareAssignSecurityGroupsTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "addsecuritygroup") +} + +func prepareUnAssignSecurityGroupsTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "delsecuritygroup") +} + +func prepareAttachInterfaceTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "attach_interface") +} + +func prepareDetachInterfaceTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "detach_interface") +} + +func prepareInstancePowerCycleTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "powercycle") +} + +func prepareClusterPowerCycleTestURL(id string) string { + return prepareGetActionTestURLParams("v2", id, "powercycle") +} + +func prepareInstanceRebootTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "reboot") +} +func prepareClusterRebootTestURL(id string) string { + return prepareGetActionTestURLParams("v2", id, "reboot") +} + +func prepareSuspendTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "suspend") +} + +func prepareResumeTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "resume") +} + +func prepareResizeTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "resize") +} + +func prepareGetTestURL(id string) string { + return prepareGetTestURLParams("v1", fake.ProjectID, fake.RegionID, id) +} + +func prepareDeleteTestURL(id string) string { + return prepareGetTestURLParams("v1", fake.ProjectID, fake.RegionID, id) +} + +func prepareCreateTestURL() string { + return prepareListTestURLParams("v1", fake.ProjectID, fake.RegionID) +} + +func prepareListMetadataTestURL(id string) string { + return prepareGetActionTestURLParams("v1", id, "metadata") +} + +func prepareMetadataTestURL(id string) string { + return prepareGetActionTestURLParams("v2", id, "metadata") +} + +func prepareMetadataItemTestURL(id string) string { + return fmt.Sprintf("/%s/ai/clusters/%d/%d/%s/%s", "v2", fake.ProjectID, fake.RegionID, id, "metadata_item") +} + +func TestListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, ListResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + actual, err := ai.ListAll(client) + require.NoError(t, err) + ct := actual[0] + require.Equal(t, AICluster1, ct) + require.Equal(t, ExpectedAIClusterSlice, actual) +} + +func TestGet(t *testing.T) { + + th.SetupHTTP() + defer th.TeardownHTTP() + + testURL := prepareGetTestURL(AICluster1.ClusterID) + + th.Mux.HandleFunc(testURL, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, GetResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + ct, err := ai.Get(client, AICluster1.ClusterID).Extract() + + require.NoError(t, err) + require.Equal(t, AICluster1, *ct) + +} + +func TestListAllInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListInterfacesTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, ClusterInterfacesResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + interfaces, err := ai.ListInterfacesAll(client, AICluster1.ClusterID) + + require.NoError(t, err) + require.Len(t, interfaces, 1) + require.Equal(t, PortID, interfaces[0].PortID) + require.Equal(t, ExpectedAIClusterInterfacesSlice, interfaces) +} + +func TestListAllPorts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListPortsTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, PortsListResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + ports, err := ai.ListPortsAll(client, AICluster1.ClusterID) + + require.NoError(t, err) + require.Len(t, ports, 1) + require.Equal(t, AIClusterPort1, ports[0]) + require.Equal(t, ExpectedPortsSlice, ports) +} + +func TestUnAssignSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareUnAssignSecurityGroupsTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestJSONRequest(t, r, UnAssignSecurityGroupsRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + opts := instances.SecurityGroupOpts{ + Name: "Test", + } + + err := ai.UnAssignSecurityGroup(client, AICluster1.ClusterID, opts).ExtractErr() + + require.NoError(t, err) +} + +func TestAssignSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareAssignSecurityGroupsTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestJSONRequest(t, r, AssignSecurityGroupsRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + opts := instances.SecurityGroupOpts{ + Name: "Test", + } + + err := ai.AssignSecurityGroup(client, AICluster1.ClusterID, opts).ExtractErr() + + require.NoError(t, err) +} + +func TestAttachInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + aiInstanceID := AICluster1.PoplarServer[0].ID + th.Mux.HandleFunc(prepareAttachInterfaceTestURL(AICluster1.PoplarServer[0].ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestJSONRequest(t, r, AttachInterfaceRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + opts := ai.AttachInterfaceOpts{ + Type: types.SubnetInterfaceType, + SubnetID: "9bc36cf6-407c-4a74-bc83-ce3aa3854c3d", + } + + tasks, err := ai.AttachAIInstanceInterface(client, aiInstanceID, opts).Extract() + + require.NoError(t, err) + require.Equal(t, Tasks1, *tasks) +} + +func TestDetachInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + aiInstanceID := AICluster1.PoplarServer[0].ID + th.Mux.HandleFunc(prepareDetachInterfaceTestURL(aiInstanceID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestJSONRequest(t, r, DetachInterfaceRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + opts := ai.DetachInterfaceOpts{ + PortID: "9bc36cf6-407c-4a74-bc83-ce3aa3854c3d", + IpAddress: "192.168.0.23", + } + + tasks, err := ai.DetachAIInstanceInterface(client, aiInstanceID, opts).Extract() + + require.NoError(t, err) + require.Equal(t, Tasks1, *tasks) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + url := prepareCreateTestURL() + fmt.Println(url) + th.Mux.HandleFunc(prepareCreateTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + options := ai.CreateOpts{ + Flavor: "g2a-ai-fake-v1pod-8", + Name: "ivandts", + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + Volumes: []instances.CreateVolumeOpts{ + { + Source: types.Image, + BootIndex: 0, + Size: 20, + TypeName: volumes.Standard, + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + }, + }, + Interfaces: []instances.InterfaceInstanceCreateOpts{ + { + InterfaceOpts: instances.InterfaceOpts{ + Type: types.AnySubnetInterfaceType, + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + }, + }, + }, + Password: "secret", + Username: "useruser", + } + err := options.Validate() + require.NoError(t, err) + client := fake.ServiceTokenClient("ai/clusters", "v1") + tasks, err := ai.Create(client, options).Extract() + require.NoError(t, err) + require.Equal(t, Tasks1, *tasks) +} + +func TestResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + url := prepareCreateTestURL() + fmt.Println(url) + th.Mux.HandleFunc(prepareResizeTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ResizeRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + options := ai.ResizeAIClusterOpts{ + Flavor: "g2a-ai-fake-v1pod-8", + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + Volumes: []instances.CreateVolumeOpts{ + { + Source: types.Image, + BootIndex: 0, + Size: 20, + TypeName: volumes.Standard, + ImageID: "06e62653-1f88-4d38-9aa6-62833e812b4f", + }, + }, + Interfaces: []instances.InterfaceInstanceCreateOpts{ + { + InterfaceOpts: instances.InterfaceOpts{ + Type: types.AnySubnetInterfaceType, + NetworkID: "518ba531-496b-4676-8ea4-68e2ed3b2e4b", + }, + }, + }, + Password: "secret", + Username: "useruser", + } + err := options.Validate() + require.NoError(t, err) + client := fake.ServiceTokenClient("ai/clusters", "v1") + tasks, err := ai.Resize(client, AICluster1.ClusterID, options).Extract() + require.NoError(t, err) + require.Equal(t, Tasks1, *tasks) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareDeleteTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, DeleteResponse) + if err != nil { + log.Error(err) + } + }) + + options := ai.DeleteOpts{ + Volumes: nil, + DeleteFloatings: true, + FloatingIPs: nil, + ReservedFixedIPs: nil, + } + + err := options.Validate() + require.NoError(t, err) + client := fake.ServiceTokenClient("ai/clusters", "v1") + tasks, err := ai.Delete(client, AICluster1.ClusterID, options).Extract() + require.NoError(t, err) + require.Equal(t, Tasks1, *tasks) + +} + +func TestPowerCycleCluster(t *testing.T) { + th.SetupHTTP() + th.Mux.HandleFunc(prepareClusterPowerCycleTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, AIClusterPowercycleResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + result, err := ai.PowerCycleAICluster(client, AICluster1.ClusterID).Extract() + require.NoError(t, err) + + require.Equal(t, AICluster1.PoplarServer, result) +} + +func TestPowerCycleInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareInstancePowerCycleTestURL(AICluster1.PoplarServer[0].ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, AIInstancePowercycleResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + instance, err := ai.PowerCycleAIInstance(client, AICluster1.PoplarServer[0].ID).Extract() + require.NoError(t, err) + require.Equal(t, &AICluster1.PoplarServer[0], instance) +} + +func TestRebootCluster(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareClusterRebootTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, AIClusterRebootResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + result, err := ai.RebootAICluster(client, AICluster1.ClusterID).Extract() + require.NoError(t, err) + require.Equal(t, AICluster1.PoplarServer, result) +} + +func TestRebootInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareInstanceRebootTestURL(AICluster1.PoplarServer[0].ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, AIInstanceRebootResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + instance, err := instances.Reboot(client, AICluster1.PoplarServer[0].ID).Extract() + require.NoError(t, err) + require.Equal(t, &AICluster1.PoplarServer[0], instance) +} + +func TestSuspend(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareSuspendTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + tasks, err := ai.Suspend(client, AICluster1.ClusterID).Extract() + require.NoError(t, err) + require.Equal(t, &Tasks1, tasks) +} + +func TestResume(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareResumeTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, CreateResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + tasks, err := ai.Resume(client, AICluster1.ClusterID).Extract() + require.NoError(t, err) + require.Equal(t, &Tasks1, tasks) +} + +func TestGetInstanceConsole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareGetInstanceConsoleTestURL(AICluster1.PoplarServer[0].ID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, InstanceConsoleResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + actual, err := ai.GetInstanceConsole(client, AICluster1.PoplarServer[0].ID).Extract() + require.NoError(t, err) + require.Equal(t, &Console, actual) +} + +// Metadata tests + +func TestMetadataListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareListMetadataTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, MetadataListResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v1") + + actual, err := ai.MetadataListAll(client, AICluster1.ClusterID) + require.NoError(t, err) + ct := actual[0] + require.Equal(t, Metadata1, ct) + require.Equal(t, ExpectedMetadataList, actual) +} + +func TestMetadataGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareMetadataItemTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, MetadataResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + + actual, err := ai.MetadataGet(client, AICluster1.ClusterID, Metadata1.Key).Extract() + require.NoError(t, err) + require.Equal(t, &Metadata1, actual) +} + +func TestMetadataCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareMetadataTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, MetadataCreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + err := ai.MetadataCreateOrUpdate(client, AICluster1.ClusterID, map[string]interface{}{ + "test1": "test1", + "test2": "test2", + }).ExtractErr() + require.NoError(t, err) +} + +func TestMetadataUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareMetadataTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, MetadataCreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + err := ai.MetadataReplace(client, AICluster1.ClusterID, map[string]interface{}{ + "test1": "test1", + "test2": "test2", + }).ExtractErr() + require.NoError(t, err) +} + +func TestMetadataDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc(prepareMetadataItemTestURL(AICluster1.ClusterID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + client := fake.ServiceTokenClient("ai/clusters", "v2") + err := ai.MetadataDelete(client, AICluster1.ClusterID, Metadata1.Key).ExtractErr() + require.NoError(t, err) +} diff --git a/gcore/ai/v1/ais/urls.go b/gcore/ai/v1/ais/urls.go new file mode 100644 index 00000000..4be55835 --- /dev/null +++ b/gcore/ai/v1/ais/urls.go @@ -0,0 +1,103 @@ +package ai + +import ( + "fmt" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" +) + +func resourceURL(c *gcorecloud.ServiceClient, id string) string { + return c.ServiceURL(id) +} + +func rootURL(c *gcorecloud.ServiceClient) string { + return c.ServiceURL() +} + +func createURL(c *gcorecloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gcorecloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gcorecloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gcorecloud.ServiceClient) string { + return rootURL(c) +} + +func resourceActionURL(c *gcorecloud.ServiceClient, id string, action string) string { + return c.ServiceURL(id, action) +} + +func resourceAIInstanceActionURL(c *gcorecloud.ServiceClient, instance_id string, action string) string { + return c.ServiceURL(instance_id, action) +} + +func interfacesListURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "interfaces") +} + +func portsListURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "ports") +} + +func attachAIInstanceInterfaceURL(c *gcorecloud.ServiceClient, instance_id string) string { + return resourceAIInstanceActionURL(c, instance_id, "attach_interface") +} + +func detachAIInstanceInterfaceURL(c *gcorecloud.ServiceClient, instance_id string) string { + return resourceAIInstanceActionURL(c, instance_id, "detach_interface") +} + +func addSecurityGroupsURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "addsecuritygroup") +} + +func deleteSecurityGroupsURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "delsecuritygroup") +} + +func powerCycleAIURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "powercycle") +} + +func powerCycleAIInstanceURL(c *gcorecloud.ServiceClient, instance_id string) string { + return resourceAIInstanceActionURL(c, instance_id, "powercycle") +} + +func rebootAIURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "reboot") +} + +func rebootAIInstanceURL(c *gcorecloud.ServiceClient, instance_id string) string { + return resourceAIInstanceActionURL(c, instance_id, "reboot") +} + +func getAIInstanceConsoleURL(c *gcorecloud.ServiceClient, instance_id string) string { + return resourceAIInstanceActionURL(c, instance_id, "get_console") +} + +func suspendAIURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "suspend") +} + +func resumeAIURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "resume") +} + +func resizeAIURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "resize") +} + +func metadataURL(c *gcorecloud.ServiceClient, id string) string { + return resourceActionURL(c, id, "metadata") +} + +func metadataItemURL(c *gcorecloud.ServiceClient, id string, key string) string { + return resourceActionURL(c, id, fmt.Sprintf("metadata_item?key=%s", key)) +} diff --git a/gcore/flavor/v1/flavors/results.go b/gcore/flavor/v1/flavors/results.go index 323f0012..6c7d9af2 100644 --- a/gcore/flavor/v1/flavors/results.go +++ b/gcore/flavor/v1/flavors/results.go @@ -66,6 +66,7 @@ type HardwareDescription struct { Disk string `json:"disk,omitempty"` Network string `json:"network,omitempty"` RAM string `json:"ram,omitempty"` + IPU string `json:"ipu,omitempty"` } // FlavorPage is the page returned by a pager when traversing over a diff --git a/gcore/loadbalancer/v1/lbpools/testing/fixtures.go b/gcore/loadbalancer/v1/lbpools/testing/fixtures.go index 78655a23..fc504219 100644 --- a/gcore/loadbalancer/v1/lbpools/testing/fixtures.go +++ b/gcore/loadbalancer/v1/lbpools/testing/fixtures.go @@ -107,7 +107,10 @@ const CreateRequest = ` } ], "lb_algorithm": "ROUND_ROBIN", - "listener_id": "c63341da-ea44-4027-bbf6-1f1939c783da" + "listener_id": "c63341da-ea44-4027-bbf6-1f1939c783da", + "timeout_client_data": 0, + "timeout_member_connect": 0, + "timeout_member_data": 0 } `