From 289ad167d384769bb2e2f3809716824d1695699f Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Fri, 5 Jun 2026 16:29:01 -0400 Subject: [PATCH] AIPCC:15489: Add support for Gaudi accelerator in mapt's IBM Cloud module --- cmd/mapt/cmd/ibmcloud/hosts/ibm-gaudi.go | 104 +++++ cmd/mapt/cmd/ibmcloud/ibmcloud.go | 1 + docs/ibmcloud/ibm-gaudi.md | 167 ++++++++ .../ibmcloud/action/ibm-gaudi/cloud-config | 145 +++++++ .../ibmcloud/action/ibm-gaudi/ibm-gaudi.go | 385 ++++++++++++++++++ 5 files changed, 802 insertions(+) create mode 100644 cmd/mapt/cmd/ibmcloud/hosts/ibm-gaudi.go create mode 100644 docs/ibmcloud/ibm-gaudi.md create mode 100644 pkg/provider/ibmcloud/action/ibm-gaudi/cloud-config create mode 100644 pkg/provider/ibmcloud/action/ibm-gaudi/ibm-gaudi.go diff --git a/cmd/mapt/cmd/ibmcloud/hosts/ibm-gaudi.go b/cmd/mapt/cmd/ibmcloud/hosts/ibm-gaudi.go new file mode 100644 index 000000000..c0d15a6f0 --- /dev/null +++ b/cmd/mapt/cmd/ibmcloud/hosts/ibm-gaudi.go @@ -0,0 +1,104 @@ +package hosts + +import ( + "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" + maptContext "github.com/redhat-developer/mapt/pkg/manager/context" + ibmgaudi "github.com/redhat-developer/mapt/pkg/provider/ibmcloud/action/ibm-gaudi" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + cmdIBMGaudi = "ibm-gaudi" + cmdIBMGaudiDesc = "manage ibm gaudi3 accelerated instances (amd64)" +) + +func IBMGaudiCmd() *cobra.Command { + c := &cobra.Command{ + Use: cmdIBMGaudi, + Short: cmdIBMGaudiDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + return nil + }, + } + + flagSet := pflag.NewFlagSet(cmdIBMGaudi, pflag.ExitOnError) + params.AddCommonFlags(flagSet) + c.PersistentFlags().AddFlagSet(flagSet) + + c.AddCommand(ibmGaudiCreate(), ibmGaudiDestroy()) + return c +} + +func ibmGaudiCreate() *cobra.Command { + c := &cobra.Command{ + Use: params.CreateCmdName, + Short: params.CreateCmdName, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + return ibmgaudi.New( + &maptContext.ContextArgs{ + Context: cmd.Context(), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + ResultsOutput: viper.GetString(params.ConnectionDetailsOutput), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Tags: viper.GetStringMapString(params.Tags), + }, + &ibmgaudi.GaudiArgs{ + SubnetID: viper.GetString(params.SubnetID), + OtelAppCode: viper.GetString(params.OtelAppCode), + OtelAuthToken: viper.GetString(params.OtelAuthToken), + OtelEndpoint: viper.GetString(params.OtelEndpoint), + OtelIndex: viper.GetString(params.OtelIndex), + OtelExtraAttrs: viper.GetStringMapString(params.OtelExtraAttrs), + }) + }, + } + flagSet := pflag.NewFlagSet(params.CreateCmdName, pflag.ExitOnError) + flagSet.StringP(params.ConnectionDetailsOutput, "", "", params.ConnectionDetailsOutputDesc) + flagSet.StringToStringP(params.Tags, "", nil, params.TagsDesc) + flagSet.StringP(params.SubnetID, "", "", params.SubnetIDDesc) + flagSet.StringP(params.OtelAppCode, "", "", params.OtelAppCodeDesc) + flagSet.StringP(params.OtelAuthToken, "", "", params.OtelAuthTokenDesc) + flagSet.StringP(params.OtelEndpoint, "", "https://otel-input.corp.redhat.com", params.OtelEndpointDesc) + flagSet.StringP(params.OtelIndex, "", "", params.OtelIndexDesc) + flagSet.StringToStringP(params.OtelExtraAttrs, "", nil, params.OtelExtraAttrsDesc) + c.PersistentFlags().AddFlagSet(flagSet) + return c +} + +func ibmGaudiDestroy() *cobra.Command { + c := &cobra.Command{ + Use: params.DestroyCmdName, + Short: params.DestroyCmdName, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + return ibmgaudi.Destroy(&maptContext.ContextArgs{ + Context: cmd.Context(), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Serverless: viper.IsSet(params.Serverless), + ForceDestroy: viper.IsSet(params.ForceDestroy), + KeepState: viper.IsSet(params.KeepState), + }) + }, + } + flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) + flagSet.Bool(params.Serverless, false, params.ServerlessDesc) + flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc) + flagSet.Bool(params.KeepState, false, params.KeepStateDesc) + c.PersistentFlags().AddFlagSet(flagSet) + return c +} diff --git a/cmd/mapt/cmd/ibmcloud/ibmcloud.go b/cmd/mapt/cmd/ibmcloud/ibmcloud.go index dcda64c84..dce66a816 100644 --- a/cmd/mapt/cmd/ibmcloud/ibmcloud.go +++ b/cmd/mapt/cmd/ibmcloud/ibmcloud.go @@ -29,6 +29,7 @@ func GetCmd() *cobra.Command { params.AddCommonFlags(flagSet) c.PersistentFlags().AddFlagSet(flagSet) c.AddCommand( + hosts.IBMGaudiCmd(), hosts.IBMPowerCmd(), hosts.IBMZCmd()) return c diff --git a/docs/ibmcloud/ibm-gaudi.md b/docs/ibmcloud/ibm-gaudi.md new file mode 100644 index 000000000..26b3b7389 --- /dev/null +++ b/docs/ibmcloud/ibm-gaudi.md @@ -0,0 +1,167 @@ +# Overview + +This action provisions an Intel Gaudi 3 accelerated instance on IBM Cloud VPC using the RHEL AI image. The instance uses the `gx3d-160x1792x8gaudi3` profile (160 vCPU, 1792 GB RAM, 8x Gaudi 3 accelerators) and is assigned a floating IP for direct SSH access. + +Two networking modes are supported: + +- **Existing subnet** (`--subnet-id`): the instance is placed in a pre-existing VPC subnet. VPC, subnet, and gateway are not created. Only `IC_REGION` is required. +- **Auto-provision** (no `--subnet-id`): a new VPC, subnet, and public gateway are created from scratch. Both `IC_REGION` and `IC_ZONE` are required. + +## Environment variables + +| Variable | Required | Description | +|---|---|---| +| `IBMCLOUD_ACCOUNT` | yes | IBM Cloud account ID | +| `IBMCLOUD_API_KEY` | yes | IBM Cloud API key | +| `IC_REGION` | yes | IBM Cloud region (e.g. `us-east`, `us-south`, `eu-de`) | +| `IC_ZONE` | only without `--subnet-id` | Availability zone (e.g. `us-east-1`) | +| `IBMCLOUD_COS_ACCESS_KEY_ID` | only with S3 `--backed-url` | HMAC access key for IBM Cloud Object Storage | +| `IBMCLOUD_COS_SECRET_ACCESS_KEY` | only with S3 `--backed-url` | HMAC secret key for IBM Cloud Object Storage | +| `IBMCLOUD_COS_ENDPOINT` | no | COS S3 endpoint (defaults to `s3..cloud-object-storage.appdomain.cloud`) | + +## Regional availability + +Gaudi 3 instances are available in: + +- **us-east** (Washington DC) +- **us-south** (Dallas) +- **eu-de** (Frankfurt) + +## Create + +```bash +mapt ibmcloud ibm-gaudi create -h +create + +Usage: + mapt ibmcloud ibm-gaudi create [flags] + +Flags: + --conn-details-output string path to export host connection information (host, username and privateKey) + -h, --help help for create + --otel-app-code string OpenTelemetry appcode identifier (e.g. MAPT-001); when set together with --otel-auth-token, installs the otelcol-contrib filelog collector on the instance + --otel-auth-token string OpenTelemetry authentication token (UUID) used to authenticate against the OTLP endpoint + --otel-endpoint string OTLP HTTP endpoint to export logs to (default "https://otel-input.corp.redhat.com") + --otel-index string Splunk index name for log routing (e.g. rh_linux) + --subnet-id string ID of an existing VPC subnet to deploy the instance into (optional) + --tags stringToString tags to add on each resource (--tags name1=value1,name2=value2) (default []) + +Global Flags: + --backed-url string backed for stack state. (local) file:///path/subpath (s3) s3://existing-bucket, (azure) azblob://existing-blobcontainer. See more https://www.pulumi.com/docs/iac/concepts/state-and-backends/#using-a-self-managed-backend + --debug Enable debug traces and set verbosity to max. Typically to get information to troubleshooting an issue. + --debug-level uint Set the level of verbosity on debug. You can set from minimum 1 to max 9. (default 3) + --project-name string project name to identify the instance of the stack +``` + +### Outputs + +Files written to the path defined by `--conn-details-output`: + +| File | Description | +|---|---| +| `host` | Floating IP of the instance (direct SSH) | +| `username` | SSH username (`root`) | +| `id_rsa` | Private key for the instance | + +A state folder is also created at `--backed-url`. It is required (together with `--project-name`) to destroy the resources later. + +### SSH access + +```bash +OUTPUT=/path/to/conn-details-output + +ssh -i ${OUTPUT}/id_rsa \ + -o StrictHostKeyChecking=no \ + root@$(cat ${OUTPUT}/host) +``` + +### Container + +```bash +# Using an existing VPC subnet +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IC_REGION=us-east \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi create \ + --project-name ibm-gaudi \ + --backed-url file:///workspace \ + --conn-details-output /workspace \ + --subnet-id + +# Auto-provisioning VPC, subnet, and gateway +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IC_REGION=us-east \ + -e IC_ZONE=us-east-1 \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi create \ + --project-name ibm-gaudi \ + --backed-url file:///workspace \ + --conn-details-output /workspace +``` + +## OpenTelemetry log collection + +When both `--otel-app-code` and `--otel-auth-token` are provided, cloud-init installs `otelcol-contrib` on the instance at first boot and configures it to ship `/var/log/messages`, `/var/log/secure`, and `/var/log/audit/audit.log` to the OTLP endpoint. + +```bash +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IC_REGION=us-east \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi create \ + --project-name ibm-gaudi \ + --backed-url file:///workspace \ + --conn-details-output /workspace \ + --subnet-id \ + --otel-app-code MAPT-001 \ + --otel-auth-token +``` + +## Using IBM Cloud Object Storage as S3 backend + +To store Pulumi state in IBM COS instead of a local file, create [HMAC credentials](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-uhc-hmac-credentials-main) for your COS instance and pass an `s3://` backed URL: + +```bash +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IBMCLOUD_ACCOUNT=XXX \ + -e IC_REGION=us-east \ + -e IC_ZONE=us-east-1 \ + -e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \ + -e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi create \ + --project-name ibm-gaudi \ + --backed-url s3://my-cos-bucket \ + --conn-details-output /workspace +``` + +## Destroy + +```bash +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IC_REGION=us-east \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi destroy \ + --project-name ibm-gaudi \ + --backed-url file:///workspace +``` + +By default, destroy removes the Pulumi state files from the backend after a successful destroy. Use `--keep-state` to preserve them: + +```bash +podman run -d --name ibm-gaudi \ + -v ${PWD}:/workspace:z \ + -e IBMCLOUD_API_KEY=XXX \ + -e IBMCLOUD_ACCOUNT=XXX \ + -e IC_REGION=us-east \ + -e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \ + -e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \ + quay.io/redhat-developer/mapt:latest ibmcloud ibm-gaudi destroy \ + --project-name ibm-gaudi \ + --backed-url s3://my-cos-bucket \ + --keep-state +``` diff --git a/pkg/provider/ibmcloud/action/ibm-gaudi/cloud-config b/pkg/provider/ibmcloud/action/ibm-gaudi/cloud-config new file mode 100644 index 000000000..fe7f16af7 --- /dev/null +++ b/pkg/provider/ibmcloud/action/ibm-gaudi/cloud-config @@ -0,0 +1,145 @@ +#cloud-config +{{- if and .AppCode .OtelAuthToken .OtelIndex}} +write_files: + - path: /etc/otelcol-contrib/config.yaml + permissions: '0640' + content: | + receivers: + filelog/messages: + include: + - /var/log/messages + start_at: end + include_file_path: true + include_file_name: true + exclude_older_than: 24h + operators: + - type: move + id: move_to_source_name + from: attributes["log.file.path"] + to: attributes["_sourceName"] + - type: remove + id: remove_file_name + field: attributes["log.file.name"] + - type: time_parser + id: parse_timestamp + layout: '%b %e %H:%M:%S' + parse_from: body + on_error: send + attributes: + index: "{{.OtelIndex}}" + _sourceCategory: messages + _sourceHost: ${env:HOSTNAME} + filelog/secure: + include: + - /var/log/secure + start_at: end + include_file_path: true + include_file_name: true + exclude_older_than: 24h + operators: + - type: move + id: move_to_source_name + from: attributes["log.file.path"] + to: attributes["_sourceName"] + - type: remove + id: remove_file_name + field: attributes["log.file.name"] + - type: time_parser + id: parse_timestamp + layout: '%b %e %H:%M:%S' + parse_from: body + on_error: send + attributes: + index: "{{.OtelIndex}}" + _sourceCategory: secure + _sourceHost: ${env:HOSTNAME} + filelog/audit: + include: + - /var/log/audit/audit.log + start_at: end + include_file_path: true + include_file_name: true + exclude_older_than: 24h + operators: + - type: move + id: move_to_source_name + from: attributes["log.file.path"] + to: attributes["_sourceName"] + - type: remove + id: remove_file_name + field: attributes["log.file.name"] + attributes: + index: "{{.OtelIndex}}" + _sourceCategory: audit + _sourceHost: ${env:HOSTNAME} + processors: + filter/drop_null_bytes: + logs: + log_record: + - 'IsMatch(body, "^\x00+$")' + batch: + timeout: "1s" + send_batch_size: 1024 + resource: + attributes: + - key: appcode + value: "{{.AppCode}}" + action: upsert + - key: com.redhat.otel.auth_token + value: "${env:OTEL_AUTH_TOKEN}" + action: upsert + - key: arch + value: "{{.OtelArch}}" + action: upsert +{{- range $k, $v := .OtelExtraAttrs}} + - key: {{$k}} + value: "{{$v}}" + action: upsert +{{- end}} + exporters: + otlphttp: + endpoint: "{{.OtelEndpoint}}" + tls: + insecure_skip_verify: true + service: + telemetry: + logs: + level: "fatal" + metrics: + level: "basic" + pipelines: + logs: + receivers: [filelog/messages, filelog/secure, filelog/audit] + processors: [filter/drop_null_bytes, resource, batch] + exporters: [otlphttp] + - path: /etc/otelcol-contrib/auth_token + permissions: '0600' + content: | + OTEL_AUTH_TOKEN={{.OtelAuthToken}} + - path: /etc/systemd/system/otelcol-contrib.service.d/capabilities.conf + permissions: '0644' + content: | + [Service] + AmbientCapabilities=CAP_DAC_READ_SEARCH + Environment="HOSTNAME=%H" + EnvironmentFile=/etc/otelcol-contrib/auth_token +{{- end}} +{{- if and .AppCode .OtelAuthToken .OtelIndex}} +runcmd: + - | + PROXY_URL="" + if ! curl -sf --connect-timeout 5 --head {{.OtelEndpoint}} > /dev/null 2>&1; then + PROXY_URL="http://squid.corp.redhat.com:3128" + fi + RPM_URL="https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{.OtelColVersion}}/otelcol-contrib_{{.OtelColVersion}}_linux_{{.OtelArch}}.rpm" + HTTPS_PROXY="$PROXY_URL" curl -fsSL -o /tmp/otelcol-contrib.rpm "$RPM_URL" + rpm -Uvh /tmp/otelcol-contrib.rpm + rm -f /tmp/otelcol-contrib.rpm + chown -R otelcol-contrib:otelcol-contrib /etc/otelcol-contrib + if [ -n "$PROXY_URL" ]; then + printf '[Service]\nEnvironment="HTTPS_PROXY=%s/"\nEnvironment="NO_PROXY=10.*,192.168.*,localhost,127.0.0.1"\n' "$PROXY_URL" \ + > /etc/systemd/system/otelcol-contrib.service.d/proxy.conf + fi + systemctl daemon-reload + systemctl enable --now otelcol-contrib +{{- end}} diff --git a/pkg/provider/ibmcloud/action/ibm-gaudi/ibm-gaudi.go b/pkg/provider/ibmcloud/action/ibm-gaudi/ibm-gaudi.go new file mode 100644 index 000000000..119a1c377 --- /dev/null +++ b/pkg/provider/ibmcloud/action/ibm-gaudi/ibm-gaudi.go @@ -0,0 +1,385 @@ +package ibmgaudi + +import ( + _ "embed" + "encoding/base64" + "fmt" + "strings" + + "github.com/mapt-oss/pulumi-ibmcloud/sdk/go/ibmcloud" + "github.com/pulumi/pulumi-command/sdk/go/command/remote" + "github.com/pulumi/pulumi-tls/sdk/v5/go/tls" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/redhat-developer/mapt/pkg/manager" + mc "github.com/redhat-developer/mapt/pkg/manager/context" + ibmcloudp "github.com/redhat-developer/mapt/pkg/provider/ibmcloud" + icdata "github.com/redhat-developer/mapt/pkg/provider/ibmcloud/data" + "github.com/redhat-developer/mapt/pkg/provider/ibmcloud/modules/network" + "github.com/redhat-developer/mapt/pkg/provider/util/command" + "github.com/redhat-developer/mapt/pkg/provider/util/output" + "github.com/redhat-developer/mapt/pkg/util" + "github.com/redhat-developer/mapt/pkg/util/file" + "github.com/redhat-developer/mapt/pkg/util/logging" + resourcesUtil "github.com/redhat-developer/mapt/pkg/util/resources" +) + +//go:embed cloud-config +var CloudConfig []byte + +var otelColVersion = "0.151.0" + +type userDataValues struct { + AppCode string + OtelAuthToken string + OtelEndpoint string + OtelColVersion string + OtelIndex string + OtelArch string + OtelExtraAttrs map[string]string +} + +const ( + stackGaudi = "icgaudi" + outputHost = "icgHost" + outputUsername = "icgUsername" + outputUserPrivateKey = "icgUserPrivatekey" + + defaultProfile = "gx3d-160x1792x8gaudi3" + defaultImage = "ibm-redhat-ai-intel" + defaultUser = "root" +) + +type GaudiArgs struct { + Prefix string + SubnetID string + OtelAppCode string + OtelAuthToken string + OtelEndpoint string + OtelIndex string + OtelExtraAttrs map[string]string +} + +type gaudiRequest struct { + mCtx *mc.Context + prefix *string + zone *string + subnetID *string + otelAppCode string + otelAuthToken string + otelEndpoint string + otelIndex string + otelExtraAttrs map[string]string +} + +func New(ctx *mc.ContextArgs, args *GaudiArgs) error { + ibmcloudProvider := ibmcloudp.Provider() + mCtx, err := mc.Init(ctx, ibmcloudProvider) + if err != nil { + return err + } + + prefix := util.If(len(args.Prefix) > 0, args.Prefix, "main") + + var zone *string + var subnetID *string + if args.SubnetID != "" { + s := strings.TrimSpace(args.SubnetID) + if s == "" { + return fmt.Errorf("--subnet-id must not be blank") + } + subnetID = &s + } else { + z, err := ibmcloudProvider.Zone() + if err != nil { + return err + } + zone = z + } + + r := &gaudiRequest{ + mCtx: mCtx, + prefix: &prefix, + zone: zone, + subnetID: subnetID, + otelAppCode: args.OtelAppCode, + otelAuthToken: args.OtelAuthToken, + otelEndpoint: args.OtelEndpoint, + otelIndex: args.OtelIndex, + otelExtraAttrs: args.OtelExtraAttrs, + } + cs := manager.Stack{ + StackName: mCtx.StackNameByProject(stackGaudi), + ProjectName: mCtx.ProjectName(), + BackedURL: mCtx.BackedURL(), + ProviderCredentials: ibmcloudp.DefaultCredentials, + DeployFunc: r.deploy, + } + sr, err := manager.UpStack(r.mCtx, cs) + if err != nil { + return fmt.Errorf("stack creation failed: %w", err) + } + return manageResults(mCtx, sr, prefix) +} + +func Destroy(mCtxArgs *mc.ContextArgs) (err error) { + mCtx, err := mc.Init(mCtxArgs, ibmcloudp.Provider()) + if err != nil { + return err + } + if err := ibmcloudp.DestroyStack(mCtx, stackGaudi); err != nil { + return err + } + return ibmcloudp.CleanupState(mCtx) +} + +func (r *gaudiRequest) deploy(ctx *pulumi.Context) error { + if r.subnetID != nil { + return r.deployWithExistingSubnet(ctx) + } + zone := *r.zone + rg, err := ibmcloud.NewResourceGroup( + ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "rg"), + &ibmcloud.ResourceGroupArgs{ + Name: pulumi.String(r.mCtx.ProjectName()), + }) + if err != nil { + return err + } + n, err := network.New(ctx, + &network.NetworkArgs{ + Prefix: *r.prefix, + Zone: &zone, + RG: rg, + ComponentID: stackGaudi, + Name: fmt.Sprintf("%s-%s", *r.prefix, r.mCtx.ProjectName()), + }) + if err != nil { + return err + } + pk, pik, err := isKey(ctx, r.mCtx, *r.prefix, stackGaudi, rg) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputUserPrivateKey), pk.PrivateKeyPem) + imageId, err := icdata.GetVPCImage(&icdata.VPCImageArgs{ + Name: defaultImage, + Arch: icdata.VPC_ARCH_X86_64, + }) + if err != nil { + return err + } + instanceArgs := &ibmcloud.IsInstanceArgs{ + Name: pulumi.String(r.mCtx.ProjectName()), + Image: pulumi.String(*imageId), + Profile: pulumi.String(defaultProfile), + Vpc: n.VPC.ID(), + Zone: pulumi.String(zone), + ResourceGroup: rg.ID(), + Keys: pulumi.StringArray{pik.ID()}, + PrimaryNetworkInterface: &ibmcloud.IsInstancePrimaryNetworkInterfaceArgs{ + Subnet: n.Subnet.ID(), + SecurityGroups: pulumi.StringArray{ + n.SecurityGroup.ID(), + }, + }, + } + if r.otelAppCode != "" && r.otelAuthToken != "" { + ud, err := gaudiUserData(r.otelAppCode, r.otelAuthToken, r.otelEndpoint, r.otelIndex, r.otelExtraAttrs) + if err != nil { + return fmt.Errorf("failed to render user data: %w", err) + } + instanceArgs.UserData = pulumi.StringPtr(ud) + } + i, err := ibmcloud.NewIsInstance(ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "is"), + instanceArgs) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputUsername), pulumi.String(defaultUser)) + _, err = ibmcloud.NewIsInstanceNetworkInterfaceFloatingIp(ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "fipassoc"), + &ibmcloud.IsInstanceNetworkInterfaceFloatingIpArgs{ + FloatingIp: n.Floatingip.ID(), + Instance: i.ID(), + NetworkInterface: i.PrimaryNetworkInterface.ApplyT( + func(pni ibmcloud.IsInstancePrimaryNetworkInterface) string { + return *pni.Id + }, + ).(pulumi.StringOutput), + }) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputHost), n.Floatingip.Address) + return nil +} + +func (r *gaudiRequest) deployWithExistingSubnet(ctx *pulumi.Context) error { + subnetInfo, err := ibmcloud.LookupIsSubnet(ctx, &ibmcloud.LookupIsSubnetArgs{ + Identifier: r.subnetID, + }) + if err != nil { + return err + } + name := fmt.Sprintf("%s-%s", *r.prefix, r.mCtx.ProjectName()) + sg, err := network.NewSecurityGroupWithSSH(ctx, &network.SecurityGroupArgs{ + Prefix: *r.prefix, + ComponentID: stackGaudi, + Name: name, + VPC: pulumi.String(subnetInfo.Vpc), + }) + if err != nil { + return err + } + fip, err := network.NewFloatingIP(ctx, &network.FloatingIPArgs{ + Prefix: *r.prefix, + ComponentID: stackGaudi, + Name: name, + Zone: pulumi.String(subnetInfo.Zone), + }) + if err != nil { + return err + } + pk, pik, err := isKey(ctx, r.mCtx, *r.prefix, stackGaudi, nil) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputUserPrivateKey), pk.PrivateKeyPem) + imageId, err := icdata.GetVPCImage(&icdata.VPCImageArgs{ + Name: defaultImage, + Arch: icdata.VPC_ARCH_X86_64, + }) + if err != nil { + return err + } + instanceArgs := &ibmcloud.IsInstanceArgs{ + Name: pulumi.String(r.mCtx.ProjectName()), + Image: pulumi.String(*imageId), + Profile: pulumi.String(defaultProfile), + Vpc: pulumi.String(subnetInfo.Vpc), + Zone: pulumi.String(subnetInfo.Zone), + Keys: pulumi.StringArray{pik.ID()}, + PrimaryNetworkInterface: &ibmcloud.IsInstancePrimaryNetworkInterfaceArgs{ + Subnet: pulumi.String(*r.subnetID), + SecurityGroups: pulumi.StringArray{sg.ID()}, + }, + } + if r.otelAppCode != "" && r.otelAuthToken != "" { + ud, err := gaudiUserData(r.otelAppCode, r.otelAuthToken, r.otelEndpoint, r.otelIndex, r.otelExtraAttrs) + if err != nil { + return fmt.Errorf("failed to render user data: %w", err) + } + instanceArgs.UserData = pulumi.StringPtr(ud) + } + i, err := ibmcloud.NewIsInstance(ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "is"), + instanceArgs) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputUsername), pulumi.String(defaultUser)) + _, err = ibmcloud.NewIsInstanceNetworkInterfaceFloatingIp(ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "fipassoc"), + &ibmcloud.IsInstanceNetworkInterfaceFloatingIpArgs{ + FloatingIp: fip.ID(), + Instance: i.ID(), + NetworkInterface: i.PrimaryNetworkInterface.ApplyT( + func(pni ibmcloud.IsInstancePrimaryNetworkInterface) string { + return *pni.Id + }, + ).(pulumi.StringOutput), + }) + if err != nil { + return err + } + ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputHost), fip.Address) + _, err = remote.NewCommand(ctx, + resourcesUtil.GetResourceName(*r.prefix, stackGaudi, "readiness-cmd"), + &remote.CommandArgs{ + Connection: remote.ConnectionArgs{ + Host: fip.Address, + User: pulumi.String(defaultUser), + PrivateKey: pk.PrivateKeyOpenssh, + }, + Create: pulumi.String(command.CommandPing), + Update: pulumi.String(command.CommandPing), + }, pulumi.Timeouts( + &pulumi.CustomTimeouts{ + Create: command.RemoteTimeout, + Update: command.RemoteTimeout}), + pulumi.DependsOn([]pulumi.Resource{i})) + return err +} + +func gaudiUserData(otelAppCode, otelAuthToken, otelEndpoint, otelIndex string, otelExtraAttrs map[string]string) (string, error) { + script, err := file.Template( + userDataValues{ + AppCode: otelAppCode, + OtelAuthToken: otelAuthToken, + OtelEndpoint: otelEndpoint, + OtelColVersion: otelColVersion, + OtelIndex: otelIndex, + OtelArch: "amd64", + OtelExtraAttrs: otelExtraAttrs, + }, + string(CloudConfig)) + if err != nil { + return "", err + } + const boundary = "MAPT-CLOUD-CONFIG" + encoded := base64.StdEncoding.EncodeToString([]byte(script)) + return strings.Join([]string{ + "MIME-Version: 1.0", + `Content-Type: multipart/mixed; boundary="` + boundary + `"`, + "", + "--" + boundary, + `Content-Type: text/cloud-config; charset="us-ascii"`, + "Content-Transfer-Encoding: base64", + "", + encoded, + "--" + boundary + "--", + "", + }, "\n"), nil +} + +func manageResults(mCtx *mc.Context, stackResult auto.UpResult, prefix string) error { + return output.Write(stackResult, mCtx.GetResultsOutputPath(), map[string]string{ + fmt.Sprintf("%s-%s", prefix, outputUsername): "username", + fmt.Sprintf("%s-%s", prefix, outputUserPrivateKey): "id_rsa", + fmt.Sprintf("%s-%s", prefix, outputHost): "host", + }) +} + +func isKey(ctx *pulumi.Context, mCtx *mc.Context, prefix, cId string, rg *ibmcloud.ResourceGroup) (*tls.PrivateKey, *ibmcloud.IsSshKey, error) { + pk, err := tls.NewPrivateKey( + ctx, + resourcesUtil.GetResourceName(prefix, cId, "pk"), + &tls.PrivateKeyArgs{ + Algorithm: pulumi.String("RSA"), + RsaBits: pulumi.Int(4096), + }) + if err != nil { + return nil, nil, err + } + if mCtx.Debug() { + pk.PrivateKeyPem.ApplyT( + func(privateKey string) error { + logging.Debugf("%s", privateKey) + return nil + }) + } + sshKeyArgs := &ibmcloud.IsSshKeyArgs{ + Name: pulumi.String(mCtx.ProjectName()), + PublicKey: pk.PublicKeyOpenssh, + } + if rg != nil { + sshKeyArgs.ResourceGroup = rg.ID() + } + pik, err := ibmcloud.NewIsSshKey(ctx, + resourcesUtil.GetResourceName(prefix, cId, "pik"), + sshKeyArgs) + return pk, pik, err +}