diff --git a/api/apis/app/v1alpha1/catalog_types.go b/api/apis/app/v1alpha1/catalog_types.go index c0e6ba0..667f6b0 100644 --- a/api/apis/app/v1alpha1/catalog_types.go +++ b/api/apis/app/v1alpha1/catalog_types.go @@ -74,6 +74,10 @@ type VMCatalog struct { Network string `json:"network"` // +optional Capacity Capacity `json:"capacity"` + // +optional + // +kubebuilder:default=linux + // +kubebuilder:validation:Enum=linux;ibmi;aix + OS string `json:"os"` } //+kubebuilder:object:root=true diff --git a/api/config/crd/bases/app.pac.io_catalogs.yaml b/api/config/crd/bases/app.pac.io_catalogs.yaml index 7d19d22..b0a882b 100644 --- a/api/config/crd/bases/app.pac.io_catalogs.yaml +++ b/api/config/crd/bases/app.pac.io_catalogs.yaml @@ -48,6 +48,7 @@ spec: description: type: string expiry: + default: 5 type: integer image_thumbnail_reference: description: Thumbnail reference for image in Catalog which consists @@ -81,6 +82,13 @@ spec: type: string network: type: string + os: + default: linux + enum: + - linux + - ibmi + - aix + type: string processor_type: type: string system_type: diff --git a/api/controllers/app/service/templates/userdata.yaml b/api/controllers/app/service/templates/userdata.yaml new file mode 100644 index 0000000..3ed370b --- /dev/null +++ b/api/controllers/app/service/templates/userdata.yaml @@ -0,0 +1,45 @@ +#cloud-config +runcmd: + - | + { + set -x + echo "--- STARTING CLOUD-INIT DEPLOYMENT ---" + + # 1. Create User Profile + /QOpenSys/usr/bin/system "CRTUSRPRF USRPRF({{.Username}}) PASSWORD(*NONE) USRCLS(*SECOFR) SPCAUT(*ALLOBJ *SECADM *JOBCTL) STATUS(*ENABLED) HOMEDIR('/home/{{.Username}}') GID(*GEN) UID(*GEN)" + + sleep 5 + + # 2. Disable Global Sign-on Lock + /QOpenSys/usr/bin/system "CHGSYSVAL SYSVAL(QRMTSIGN) VALUE('*VERIFY')" + + # 3. Create SSH Directory + mkdir -p /home/{{.Username}}/.ssh + + # 4. Inject SSH Key(s) and FORCE CCSID 1208 (Critical for IBM i) + printf "{{.Keys}}\n" > /home/{{.Username}}/.ssh/authorized_keys + /QOpenSys/usr/bin/system "CHGATR OBJ('/home/{{.Username}}/.ssh/authorized_keys') ATR(*CCSID) VALUE(1208)" + + # 5. Change Ownership + /QOpenSys/usr/bin/system "CHGOWN OBJ('/home/{{.Username}}') NEWOWN({{.Username}}) SUBTREE(*ALL)" + + # 6. Secure Permissions + chmod 700 /home/{{.Username}} + chmod 700 /home/{{.Username}}/.ssh + chmod 600 /home/{{.Username}}/.ssh/authorized_keys + + # 7. SSHD Config Update + CONF="/QOpenSys/QIBM/UserData/SC1/OpenSSH/etc/sshd_config" + if [ -f "$CONF" ]; then + sed 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' "$CONF" > /tmp/sshd.tmp && mv /tmp/sshd.tmp "$CONF" + sed 's|^#\?AuthorizedKeysFile.*|AuthorizedKeysFile .ssh/authorized_keys|' "$CONF" > /tmp/sshd.tmp && mv /tmp/sshd.tmp "$CONF" + chmod 644 "$CONF" + fi + + # 8. Restart SSH + /QOpenSys/usr/bin/system "ENDTCPSVR *SSHD" + sleep 5 + /QOpenSys/usr/bin/system "STRTCPSVR *SSHD" + + echo "--- DEPLOYMENT FINISHED ---" + } > /tmp/cloud-init-trace.log 2>&1 diff --git a/api/controllers/app/service/vm.go b/api/controllers/app/service/vm.go index 0430ecf..5b26f55 100644 --- a/api/controllers/app/service/vm.go +++ b/api/controllers/app/service/vm.go @@ -1,11 +1,14 @@ package service import ( + "bytes" "context" + _ "embed" "encoding/base64" "fmt" "strconv" "strings" + "text/template" "github.com/IBM-Cloud/power-go-client/power/models" "github.com/IBM/go-sdk-core/v5/core" @@ -19,9 +22,14 @@ const ( publicNetworkPrefix = "pac-public-network" ) +//go:embed templates/userdata.yaml +var userDataTemplate string + var ( ErroNoPublicNetwork = errors.New("no public network available to use for vm creation") dnsServers = []string{"9.9.9.9", "1.1.1.1"} + IbmiUsername = "PAC" + IbmiOS = "ibmi" ) func getAvailablePubNetwork(scope *scope.ServiceScope) (string, error) { @@ -176,6 +184,7 @@ func createVM(scope *scope.ServiceScope) error { memory := float64(vmSpec.Capacity.Memory) processors, _ := strconv.ParseFloat(vmSpec.Capacity.CPU, 64) + userData := getUserData(vmSpec, scope) createOpts := &models.PVMInstanceCreate{ ServerName: &scope.Service.Name, ImageID: imageRef.ImageID, @@ -184,7 +193,7 @@ func createVM(scope *scope.ServiceScope) error { Processors: &processors, SysType: vmSpec.SystemType, ProcType: &vmSpec.ProcessorType, - UserData: base64.StdEncoding.EncodeToString([]byte(strings.Join(scope.Service.Spec.SSHKeys, "\n"))), + UserData: userData, } pvmInstanceList, err := scope.PowerVSClient.CreateVM(createOpts) @@ -199,3 +208,38 @@ func createVM(scope *scope.ServiceScope) error { scope.Service.Status.VM.InstanceID = *(*pvmInstanceList)[0].PvmInstanceID return nil } + +func getUserData(vmSpec appv1alpha1.VMCatalog, scope *scope.ServiceScope) string { + var userData string + switch vmSpec.OS { + case IbmiOS: + userData = getIBMiUserData(scope.Service.Spec.SSHKeys, IbmiUsername, scope) + default: + userData = base64.StdEncoding.EncodeToString( + []byte(strings.Join(scope.Service.Spec.SSHKeys, "\n")), + ) + } + return userData +} + +func getIBMiUserData(sshKeys []string, username string, scope *scope.ServiceScope) string { + allKeys := strings.Join(sshKeys, "\\n") + cloudInitTemplate, err := template.New("userdata").Parse(userDataTemplate) + if err != nil { + scope.Logger.Error(err, "error parsing cloud-init template") + return "" + } + var buf bytes.Buffer + err = cloudInitTemplate.Execute(&buf, struct { + Username string + Keys string + }{ + Username: username, + Keys: allKeys, + }) + if err != nil { + scope.Logger.Error(err, "error executing cloud-init template") + return "" + } + return base64.StdEncoding.EncodeToString(buf.Bytes()) +}