AWSTemplateFormatVersion: 2010-09-09
Description: >-
  PhotoMesh cloudformation
  ami to region were mapped using a script described at https://octopus.com/blog/ami-mappings-cloudformation 
Parameters:
  vpcName:
    Type: String
    Description: The name of the vpc
    Default: "PM VPC"
  vpcCIDR:
    Type: String
    Description: vpcCIDR
    Default: 10.0.0.0/16
  masterInstanceType:
    Type: String
    Description: master instance type
    Default: g4dn.xlarge
  masterInstanceStorageVolume:
    Type: Number
    Description: amount of storage for master instance
    Default: 30
  remoteAccess:
    Type: String
    Description: set ip with /32 mask for remote access 
    Default: 0.0.0.0/0
Mappings: ### fuserAmi is set to Ubuntu 24.04 server, aside from us-east-1 which is the actual fuser AMI
  RegionMap:
    # ap-south-1:
    #   masterAmi: ami-09f6da726716a4ca6
    #   fuserAmi: ami-09edb498ada567cb6
    # eu-north-1:
    #   masterAmi: ami-02f509ff9ea7ce781
    #   fuserAmi: ami-06b99e0a196a5f288
    # eu-west-3:
    #   masterAmi: ami-0047c43abbad5198e
    #   fuserAmi: ami-07d87a2fde2162b75
    # eu-west-2:
    #   masterAmi: ami-0795faa4ab9bc8d8a
    #   fuserAmi: ami-03826eba80141ae8a
    # eu-west-1:
    #   masterAmi: ami-015539bd6054cc380
    #   fuserAmi: ami-0b6cac7a13a1450e3
    # ap-northeast-3:
    #   masterAmi: ami-051c7e937d8ff7644
    #   fuserAmi: ami-06930a9f5a0e0641b
    # ap-northeast-2:
    #   masterAmi: ami-0e0c3c4ce28132149
    #   fuserAmi: ami-043c950725ba32310
    # ap-northeast-1:
    #   masterAmi: ami-016415eef3351ae7d
    #   fuserAmi: ami-08b718e14edad7164
    # il-central-1:
    #   masterAmi: ami-07df29cf3e326c3ad
    #   fuserAmi: ami-0165b8023ac51a65b
    # ca-central-1:
    #   masterAmi: ami-0f7ef5cb0b7d00ba2
    #   fuserAmi: ami-0a9d7bff7b2d054f4
    # sa-east-1:
    #   masterAmi: ami-011cb121b1f9961bc
    #   fuserAmi: ami-0cf8f2b5b6c1ac22a
    # ap-southeast-1:
    #   masterAmi: ami-00cefb167fda388d1
    #   fuserAmi: ami-0d38c6cac7ae01026
    # ap-southeast-2:
    #   masterAmi: ami-0e5c9f1cb7457e5dd
    #   fuserAmi: ami-0436c0311be4c63a3
    # eu-central-1:
    #   masterAmi: ami-0e94f1103762e84a6
    #   fuserAmi: ami-019bb819e3eefa479
    us-east-1:
      masterAmi: ami-001adaa5c3ee02e10
      fuserAmi: ami-0ef917f432cbae1a7
    # us-east-2:
    #   masterAmi: ami-08b66c1b6d6a8a30a
    #   fuserAmi: ami-01146f9709687a22b
    # us-west-1:
    #   masterAmi: ami-070e1a98c2bcba860
    #   fuserAmi: ami-0781cc7deee3d0d34
    # us-west-2:
    #   masterAmi: ami-07e278fe6c43b6aba
    #   fuserAmi: ami-0406d1fdd021121cd
Resources:
###############################
#### PM VPC ###################
###############################
  PMVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref vpcCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Ref vpcName
###############################
#### PM Subnet ################
###############################
  PMVPCPMPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref PMVPC
      AvailabilityZone:
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      CidrBlock: !Select 
        - 0
        - 'Fn::Cidr':
            - !GetAtt 
              - PMVPC
              - CidrBlock
            - 2
            - 8
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PM Subnet1
  PMVPCPMPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref PMVPC
      AvailabilityZone:
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      CidrBlock: !Select 
        - 1
        - 'Fn::Cidr':
            - !GetAtt 
              - PMVPC
              - CidrBlock
            - 2
            - 8
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PM Subnet2

  PMVPCPMPublicSubnetSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref PMVPC
      Tags:
        - Key: Name
          Value: PM Subnet

  PMVPCPMPublicSubnetSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref PMVPC
      Tags:
        - Key: Name
          Value: PM Subnet

  PMVPCPMPublicSubnetSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PMVPCPMPublicSubnetSubnet1RouteTable
      SubnetId: !Ref PMVPCPMPublicSubnet1
  PMVPCPMPublicSubnetSubnet1DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PMVPCPMPublicSubnetSubnet1RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref PMVPCIGW
    DependsOn:
      - PMVPCVPCGW

  PMVPCPMPublicSubnetSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PMVPCPMPublicSubnetSubnet2RouteTable
      SubnetId: !Ref PMVPCPMPublicSubnet2
  PMVPCPMPublicSubnetSubnet2DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PMVPCPMPublicSubnetSubnet2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref PMVPCIGW
    DependsOn:
      - PMVPCVPCGW
###############################
#### PM Internet GW ###########
###############################
  PMVPCIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: PM_IGW
  PMVPCVPCGW:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref PMVPC
      InternetGatewayId: !Ref PMVPCIGW
###############################
#### PM Security Group ########
###############################
  PMSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: PM_SG
      GroupName: PM_SG
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: !GetAtt PMVPC.CidrBlock
          Description: !Join [": ", ["from", !GetAtt PMVPC.CidrBlock]]
          FromPort: 3389
          IpProtocol: tcp
          ToPort: 3389
        - CidrIp: !GetAtt PMVPC.CidrBlock
          Description: !Join [": ", ["from", !GetAtt PMVPC.CidrBlock]]
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
        - CidrIp: !GetAtt PMVPC.CidrBlock
          Description: !Join [": ", ["from", !GetAtt PMVPC.CidrBlock]]
          FromPort: 5900
          IpProtocol: tcp
          ToPort: 5901
        - CidrIp: !Ref remoteAccess
          Description: "SSH access"
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
        - CidrIp: !Ref remoteAccess
          Description: "RDP access"
          FromPort: 3389
          IpProtocol: tcp
          ToPort: 3389
      Tags:
        - Key: Name
          Value: PM_SG
      VpcId: !Ref PMVPC
  PMSGINGRESS:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref PMSG
      IpProtocol: tcp
      FromPort: 445
      ToPort: 445
      SourceSecurityGroupId: !Ref PMSG
############################
#### PM Instance Role ######
############################
  PMRUNFUSERROLE:
    Type: AWS::IAM::Role
    Properties:
      RoleName: PM_RUN_FUSER_ROLE
      ManagedPolicyArns: ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"]
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      Tags:
        - Key: Name
          Value: PM_RUN_FUSER_ROLE  
  PMRUNFUSERPOLICY:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: PM_RUN_FUSER_POLICY
      PolicyDocument:
        Statement:
          - Action:
              - ec2:DescribeInstances
              - ec2:TerminateInstances
              - ec2:RequestSpotInstances
              - ec2:DescribeTags
              - ec2:CreateTags
              - ec2:DescribeRegions
              - ec2:RunInstances
              - s3:ListBucket
              - ec2:DescribeSpotInstanceRequests
              - ec2:DescribeSecurityGroups
              - ec2:DescribeSpotPriceHistory
              - ec2:DescribeImages
              - ec2:CancelSpotInstanceRequests
              - s3:GetObject
              - iam:CreateServiceLinkedRole
              - ec2:DescribeSubnets
              - ec2:DescribeKeyPairs
              - ec2:DescribeInstanceStatus
              - ec2:GetPasswordData
              - ssm:GetParametersByPath
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
      Roles:
        - Ref: PMRUNFUSERROLE
  PMKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: PM-Key-Pair
      Tags:
        - Key: Name
          Value: PM-Key-Pair
###########################
#### Master Instance ######
###########################
  masterInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: PMRUNFUSERROLE
  master:
    Type: AWS::EC2::Instance
    Properties:
      AvailabilityZone:
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      IamInstanceProfile:
        Ref: masterInstanceProfile
      PrivateIpAddress: "10.0.0.10"
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: 30
            VolumeType: gp3
            Encrypted: true
            DeleteOnTermination: true
            Iops: 3000
        - DeviceName: /dev/xvdd
          Ebs:
            VolumeSize: !Ref masterInstanceStorageVolume
            VolumeType: gp3
            Encrypted: true
            DeleteOnTermination: true
            Iops: 3000
      ImageId: !FindInMap [RegionMap, !Ref AWS::Region, masterAmi]
      InstanceType: !Ref masterInstanceType
      KeyName: !Ref PMKeyPair
      SecurityGroupIds:
        - Fn::GetAtt:
            - PMSG
            - GroupId
      SubnetId: !Ref PMVPCPMPublicSubnet1
      Tags:
        - Key: Name
          Value: PM Master
      UserData:
        Fn::Base64: !Sub
        - |
            <powershell>
              try {
                Stop-Service -Name ShellHWDetection
              } catch {
                throw "Failed to stop the ShellHWDetection service_"
              }
              $driveletter="d"
              # Assume last character of the device name = drive letter
              # SCSI targets for device names:
              # http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-volumes.html#windows-volume-mapping
              $letters = @("b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z")
              # I.e. /dev/xvdb = scsi target 1
              $scsitarget = 1 + $letters.IndexOf( $driveletter )
              if( -not $scsitarget) {
                throw "Invalid drive letter. Could not look up scsitarget for $driveletter"
              }


              $disk = Get-Disk | Where-Object { $_.FriendlyName -notlike '*EC2*' -and $_.PartitionStyle -eq 'RAW' }

              if( -not $disk ) {
                throw "Could not find disk with SCSI target $scsitarget"
              }

              try {
                Initialize-Disk -Number $disk.Number -PartitionStyle "MBR"
              } catch {
                throw "Failed to initialize disk $_"
              }

              try {
                $part = New-Partition -DiskNumber $disk.Number -UseMaximumSize -IsActive -DriveLetter $driveletter
              } catch {
                throw "Failed to create partition $_"
              }

              try {
                Format-Volume -DriveLetter $part.DriveLetter -NewFileSystemLabel $label -Confirm
              } catch {
                throw "Failed to format drive $driveletter $_"
              }

              try{
                $Bucket = "nvidia-gaming"
                $KeyPrefix = "windows/latest"
                $LocalPath = "$home\Desktop\NVIDIA"
                $Objects = Get-S3Object -BucketName $Bucket -KeyPrefix $KeyPrefix -Region us-east-1
                foreach ($Object in $Objects) {
                  $LocalFileName = $Object.Key
                   if ($LocalFileName -ne '' -and $Object.Size -ne 0) {
                      $LocalFilePath = Join-Path $LocalPath $LocalFileName
                      Copy-S3Object -BucketName $Bucket -Key $Object.Key -LocalFile $LocalFilePath -Region us-east-1
                    }
                  }
              } catch {
                throw "Failed to download NVIDIA drivers $_"
              }
            
              try {
                $driver_path = Get-ChildItem C:\Users\Administrator\Desktop\NVIDIA\windows\latest\ -Filter *.exe -Recurse | %{$_.FullName}
                Start-Process -FilePath $driver_path -ArgumentList "/s" -Wait
              } catch {
                throw "Failed to install NVIDIA drivers $_"
              }

              try{
                New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\nvlddmkm\Global" -Name "vGamingMarketplace" -PropertyType "DWord" -Value "2"
              } catch {
                throw "Failed to create regisy item $_"
              }
              try{
                Invoke-WebRequest -Uri "https://nvidia-gaming.s3.amazonaws.com/GridSwCert-Archive/GridSwCertWindows_2024_02_22.cert" -OutFile "$Env:PUBLIC\Documents\GridSwCert.txt"
              } catch{
                throw "Failed to download certificate $_"
              }
              try {
                Start-Service -Name ShellHWDetection
              } catch {
                throw "Failed to start the ShellHWDetection service $_"
              }
              try {
                Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" -Name "IsInstalled" -Value 0
                Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" -Name "IsInstalled" -Value 0
              } catch {
                throw "Failed to update registry for enhanced IE security $_"
              }
              try {
                New-Item -ItemType "directory" -Path "D:\PhotoMesh"
                New-Item -ItemType "directory" -Path "D:\PMProjects"
                New-Item -ItemType "directory" -Path "D:\PMWorkingFolder\A"
                New-Item -ItemType "directory" -Path "D:\PhotoMesh\AWSConfigurations"
              } catch {
                throw "Failed to create D:\PhotoMesh paths $_"
              }

              try {
                New-SmbShare -Name d -Path "D:\" -FullAccess Everyone
              } catch {
                throw "Failed to share folders for directories in D volume $_"
              }
            
              try {
                Read-S3Object -BucketName "skylinesoft-resources" -KeyPrefix "photomesh/latest/" -Folder  D:\PhotoMesh
                Expand-Archive -Path D:\PhotoMesh\PhotoMesh.zip -DestinationPath D:\PhotoMesh
              } catch {
                throw "Failed to create D:\PhotoMesh path $_"
              }

              try {
                Read-S3Object -BucketName "skylinesoft-resources" -Key photomesh/PM-AWS-Files.zip -File  D:\PhotoMesh\AWSConfigurations\PM-AWS-Files.zip
                Expand-Archive -Path D:\PhotoMesh\AWSConfigurations\PM-AWS-Files.zip -DestinationPath D:\PhotoMesh\AWSConfigurations -Force
              } catch {
                throw "Failed downloading PM AWS files $_"
              }

              try {
                $baseJsonFilesPath="D:\PhotoMesh\AWSConfigurations"
                $jsonFiles=@("DataPrep_AT.json", "Reconstruction_CPU.json", "Reconstruction_GPU.json")
                for ($i = 0; $i -lt $jsonFiles.Length; $i++) {
                  $jsonFile=(Join-Path $baseJsonFilesPath $jsonFiles[$i])
                  $jsonFileRaw = (Get-Content $jsonFile -Raw )| ConvertFrom-Json
                  for ($y = 0; $y -lt $jsonFileRaw.configurations.Count; $y++) {
                    $jsonFileRaw.configurations[$y].ami = "${amiId}"
                  }
                  $jsonFileRaw | ConvertTo-Json -Depth 100 | Out-File "$jsonFile" -Encoding utf8
                }
              } catch {
                throw "Failed updating json configuration files $_"
              }

              try {
                $process = Start-Process -FilePath "D:\PhotoMesh\PhotoMesh.exe" -PassThru
                Start-Sleep -Seconds 30
                Stop-Process -Id $process.Id
              } catch {
                throw "Failed starting PhotoMesh $_"
              }
              try {
                New-ItemProperty -Path "HKCU:\Software\PhotoMesh\AWSCloudService" -Name "Subnet" -Value ${PMVPCPMPublicSubnet1}  -PropertyType "String"
                New-ItemProperty -Path "HKCU:\Software\PhotoMesh\AWSCloudService" -Name "AlternativeSubnets" -Value ${PMVPCPMPublicSubnet2}  -PropertyType "String"
              } catch {
                throw "Failed updating registry HKCU:\Software\PhotoMesh\AWSCloudService with Subnet and AlternativeSubnets keys $_"
              }
              try {
                $pmFilePath = "D:\key_pair.pem"
                $InstanceId = Invoke-RestMethod -Uri 'http://169.254.169.254/latest/meta-data/instance-id'
                $keyPairPrefix = "/ec2/keypair/"
                $pmFileFromParameterStore = (Get-SSMParametersByPath -Path $keyPairPrefix -WithDecryption $true).Value
                $pmFileFromParameterStore | Out-File -FilePath $pmFilePath -Encoding UTF8
                $adminpassword = (Get-EC2PasswordData -InstanceId $InstanceId -PemFile $pmFilePath)
                $startUpScriptFile = "D:\PhotoMesh\AWSConfigurations\Startup_Script_awsQG_Template.txt"
                (Get-Content $startUpScriptFile).replace('<pass>',$adminpassword) | Set-Content $startUpScriptFile
                (Get-Content $startUpScriptFile).replace('<user>','administrator') | Set-Content $startUpScriptFile
                $startupScriptContent = Get-Content $startUpScriptFile -Raw
                New-ItemProperty -Path "HKCU:\Software\PhotoMesh\AWSCloudService" -Name "StartupScript" -Value $startupScriptContent  -PropertyType "String"
                Remove-Item -Path $pmFilePath
              } catch {
                throw "Failed updating registry HKCU:\Software\PhotoMesh\AWSCloudService with StartupScript $_"
              }

              try {
                $WshShell = New-Object -comObject WScript.Shell
                $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\PhotoMesh.lnk")
                $Shortcut.TargetPath = "\\10.0.0.10\d\PhotoMesh\PhotoMesh.exe"
                $Shortcut.Save()
              } catch {
                throw "Failed to create shourtcut for PhotoMesh $_"
              }

              try {
                
                $destConfigPath = Join-Path -Path $env:APPDATA -ChildPath "\Skyline\TerraBuilder\UserSettingsPM.config"
                Read-S3Object -BucketName "skylinesoft-resources" -Key photomesh/cloudformation/UserSettingsPM.config -File $destConfigPath
                #$sourceConfigPath = "D:\PhotoMesh\UserSettingsPM.config"
                #Copy-Item -Path $sourceConfigPath -Destination $destConfigPath
                #Remove-Item -Path $sourceConfigPath
              } catch {
                throw "Failed to download config file $_"
              }

              try {
                cfn-signal.exe --success true --stack ${AWS::StackName} --region ${AWS::Region} --resource master
              } catch {
                throw "Failed signaling completion status to cloudformation $_"
              }

            </powershell>
        - amiId: !FindInMap [RegionMap, !Ref "AWS::Region", fuserAmi]
    DependsOn:
      - PMRUNFUSERPOLICY
      - PMRUNFUSERROLE
    CreationPolicy:
      ResourceSignal:
        Count: 1
        Timeout: "PT15M"
Outputs:
  PublicIp:
    Description: Master Public IP
    Value: !GetAtt master.PublicIp
  PrivateIp:
    Description: Master Private IP
    Value: !GetAtt master.PrivateIp