AWSTemplateFormatVersion: "2010-09-09"

# MIT License
#
# Copyright (c) 2021 Qumulo, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal 
# in the Software without restriction, including without limitation the rights 
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
# copies of the Software, and to permit persons to whom the Software is 
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all 
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
# SOFTWARE.

Description: This is the main template to spin up a Qumulo Cluster with all WAF supporting infrastructure.  It calls subordinate CloudFormation templates to instantiate the infrastructure. (qs-1s6n2i6af)

Metadata:
  cfn-lint:
    config:
      ignore_checks:
        - W2001
        - W9002
        - W9003
        - W9004
        - W9006
        - E9010
 
Parameters:

  QSS3BucketName:
    Type: String
  QSS3BucketRegion:
    Type: String
  QSS3KeyPrefix:
    Type: String
  KeyPair:
    Type: String
  EnvType:
    Type: String
  VPCId:
    Type: String
  QPublicMgmt:
    Type: String
  QPublicRepl:
    Type: String
  NumberAZs:
    Type: String
  PublicSubnetIDs:
    Type: String
  PrivateSubnetIDs:
    Type: CommaDelimitedList
  QNlbPrivateSubnetIDs:
    Type: CommaDelimitedList    
  DomainName:
    Type: String
  QDr:
    Type: String
  QFloatRecordName:
    Type: String
  QNodeCount:
    Type: String
  QMaxNodesDown:
    Type: String
  QDiskConfig:
    Type: String
  QFlashType:
    Type: String
  QFlashTput:
    Type: String
  QFlashIops:
    Type: String    
  QAmiID:
    Type: String
  QAmiVer:
    Type: String
  QMarketPlaceType:
    Type: String
  QModOverness:
    Type: String
  QFloatingIP:
    Type: String
  QClusterName:
    Type: String
  QClusterVersion:
    Type: String
  QClusterAdminPwd:
    Type: String
    NoEcho: "true"
  QInstanceType:
    Type: String
  VolumesEncryptionKey: 
    Type: String
  QPermissionsBoundary:
    Type: String
  QInstanceRecoveryTopic:
    Type: String
  QNlbSticky:
    Type: String
  QNlbXzone:
    Type: String
  QNlbDeregDelay:
    Type: String
  QNlbDeregTerm:
    Type: String
  QNlbPreserveIP:
    Type: String    
  QSgCidr1:
    Type: String
  QSgCidr2:
    Type: String
  QSgCidr3:
    Type: String
  QSgCidr4:
    Type: String
  TermProtection:
    Type: String
  QClusterLocalZone:
    Type: String
  QAuditLog:
    Type: String
  RequireIMDSv2:
    Type: String
    AllowedValues:
      - "YES"
      - "NO"        
  SideCarPrivateSubnetID:
    Type: String
  SideCarProv:   
    Type: String
  SideCarVersion:
    Type: String
  SideCarUsername:
    Type: String
  SideCarPassword:
    Type: String
    NoEcho: "true"
  SideCarSNSTopic:
    Type: String
  TopStackName:
    Type: String

Conditions:

  UsingDefaultBucket: !Equals [!Ref QSS3BucketName, 'aws-quickstart']
  
  ProvFlashIops: !Not
    - !Equals
      - !Ref QFlashIops
      - "Use Qumulo Default"

  ProvFlashTput: !Not
    - !Equals
      - !Ref QFlashTput
      - "Use Qumulo Default"

  ProvSg2: !Not
    - !Equals
      - !Ref QSgCidr2
      - ""

  ProvSg3: !Not
    - !Equals
      - !Ref QSgCidr3
      - ""      

  ProvSg4: !Not
    - !Equals
      - !Ref QSgCidr4
      - ""

  AddQSgCidr: !Or [Condition: ProvSg2, Condition: ProvSg3, Condition: ProvSg4]

  ProvMultiAZ: !Not
    - !Equals
      - !Ref NumberAZs
      - "1"

  ProvFloatIP: !Not
    - !Equals
      - !Ref QFloatingIP
      - "0"

  ProvDNS: !Not
    - !Equals
      - !Ref DomainName
      - ""

  ProvNLB: !Not
    - !Equals 
      - !Join [",", !Ref QNlbPrivateSubnetIDs]
      - ""

  ProvR53: !And [Condition: ProvDNS, !Not [Condition: ProvMultiAZ], !Not [Condition: ProvNLB], Condition: ProvFloatIP]

  ProvSideCar: !Equals
    - !Ref SideCarProv
    - "YES"

  PubMgmt: !Not
    - !Equals
      - !Ref QPublicMgmt
      - "NO"

  LocalAZ: !Not
    - !Equals
      - !Ref QClusterLocalZone
      - "NO"

  ProvMgmt: !And [Condition: PubMgmt, !Not [Condition: LocalAZ]]

  CustomAMI: !Equals
    - !Ref QMarketPlaceType
    - "Specified-AMI-ID"

  CustomMP: !Or 
    - !Equals
      - !Ref QMarketPlaceType
      - "Custom-1TB-6PB"
    - !Equals
      - !Ref QMarketPlaceType
      - "Specified-AMI-ID"

  CustomDC: !Not
    - !Equals
      - !Ref QDiskConfig
      - "Select for Custom Offering"

  CustomNC: !Not
    - !Equals
      - !Ref QNodeCount
      - "Select for Custom Offering OR Expanding Cluster"

  LookupAMI: !Not [Condition: CustomAMI]

  Custom: !And [Condition: CustomMP,  Condition: CustomDC, Condition: CustomNC]

Mappings:
  ConfigMap:
    1TB-Usable-All-Flash:
      DiskConfig: 600GiB-AF
      NodeCount: "4"
      ShortName: 1TB
    12TB-Usable-Hybrid-st1:
      DiskConfig: 5TB-Hybrid-st1
      NodeCount: "4"
      ShortName: 12TB      
    96TB-Usable-Hybrid-st1:
      DiskConfig: 20TB-Hybrid-st1
      NodeCount: "6"      
      ShortName: 96TB      
    103TB-Usable-All-Flash:
      DiskConfig: 30TB-AF
      NodeCount: "5"      
      ShortName: 103TB      
    270TB-Usable-Hybrid-st1:
      DiskConfig: 55TiB-Hybrid-st1
      NodeCount: "6"      
      ShortName: 270TB      
    809TB-Usable-Hybrid-st1:
      DiskConfig: 160TiB-Hybrid-st1
      NodeCount: "6"      
      ShortName: 809TB      
    Custom-1TB-6PB:
      DiskConfig: CUSTOM-ERROR--NEED-TO-SELECT-DISK-CONFIG
      NodeCount: CUSTOM-ERROR--NEED-TO-SELECT-NODE-COUNT      
      ShortName: Custom 
    Specified-AMI-ID:
      DiskConfig: SPECIFIED-AMI-ID-ERROR--NEED-TO-SELECT-DISK-CONFIG
      NodeCount: SPECIFIED-AMI-ID-ERROR--NEED-TO-SELECT-NODE-COUNT      
      ShortName: Custom 
      
Resources:

  SECRETSSTACK:
    Type: "AWS::CloudFormation::Stack"
    Properties:
      Parameters:
        ClusterAdminPwd: !Ref QClusterAdminPwd
        SideCarPassword: !If [ProvSideCar, !Ref SideCarPassword, ""]
        SideCarUsername: !If [ProvSideCar, !Ref SideCarUsername, ""]
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/store-secrets.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150         

  QLOOKUPSTACK:
    Type: "AWS::CloudFormation::Stack"
    Condition: LookupAMI
    Properties:
      Parameters:
        MarketPlaceType: !FindInMap [ConfigMap, !Ref QMarketPlaceType, ShortName]
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/qami-id-lookup-${QAmiVer}.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  QIAMSTACK:
    Type: "AWS::CloudFormation::Stack"
    Properties:
      Parameters:
        QPermissionsBoundary: !Ref QPermissionsBoundary
        VolumesEncryptionKey: !Ref VolumesEncryptionKey        
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/qiam.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  QSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Properties:
      Parameters:
        BucketName: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
        BucketRegion: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
        KeyPair: !Ref KeyPair
        KeyPrefix: !Ref QSS3KeyPrefix        
        NumberAZs: !Ref NumberAZs
        PrivateSubnetIDs: !Join [",", !Ref PrivateSubnetIDs]
        QAmiID: !If [LookupAMI, !GetAtt QLOOKUPSTACK.Outputs.AmiId, !Ref QAmiID]
        QClusterName: !Ref QClusterName
        QDiskConfig: !If [Custom, !Ref QDiskConfig, !FindInMap [ConfigMap, !Ref QMarketPlaceType, DiskConfig]]
        QFlashType: !Ref QFlashType
        QFloatingIP: !Ref QFloatingIP
        QInstanceIAMProfile: !GetAtt QIAMSTACK.Outputs.QumuloIAMProfile
        QInstanceRecoveryTopic: !Ref QInstanceRecoveryTopic
        QInstanceType: !Ref QInstanceType
        QNodeCount: !If [CustomNC, !Ref QNodeCount, !FindInMap [ConfigMap, !Ref QMarketPlaceType, NodeCount]]
        QSgCidr1: !Ref QSgCidr1
        RequireIMDSv2: !Ref RequireIMDSv2        
        VPCId: !Ref VPCId
        VolumesEncryptionKey: !Ref VolumesEncryptionKey
      TemplateURL: 
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/qcluster.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150

  QADDCIDRSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Condition: AddQSgCidr
    Properties:
      Parameters:
        QSgCidr2: !Ref QSgCidr2        
        QSgCidr3: !Ref QSgCidr3   
        QSgCidr4: !Ref QSgCidr4     
        QSgId: !GetAtt QSTACK.Outputs.ClusterSGID        
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/qadd-sg-cidrs.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150       

  QSIDECARSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Condition: ProvSideCar
    Properties:
      Parameters:
        Hosts: !GetAtt QSTACK.Outputs.ClusterPrivateIPs
        Password: !Ref SideCarPassword
        SNSTopic: !Ref SideCarSNSTopic
        SecurityGroup: !GetAtt QSTACK.Outputs.ClusterSGID
        Subnet: !If [LocalAZ, !Ref SideCarPrivateSubnetID, !Select [0, !Ref PrivateSubnetIDs]]
        Username: !Ref SideCarUsername
      TemplateURL: !Sub "https://qumulo-sidecar-us-east-1.s3.amazonaws.com/${SideCarVersion}/sidecar_cft.json"
      TimeoutInMinutes: 150

  PROVISIONINGSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Properties:
      Parameters:
        BucketName: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
        BucketRegion: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
        CMK: !Ref VolumesEncryptionKey
        ClusterPwd: !GetAtt QSTACK.Outputs.TemporaryPassword
        ClusterSecretsArn: !GetAtt SECRETSSTACK.Outputs.ClusterSecretsArn
        FlashIops: !If [ProvFlashIops, !Ref QFlashIops, !GetAtt QSTACK.Outputs.FlashIOPS]
        FlashTput: !If [ProvFlashTput, !Ref QFlashTput, "250"]
        FlashType: !Ref QFlashType
        FloatIPs: !If [ProvFloatIP, !GetAtt QSTACK.Outputs.ClusterSecondaryPrivateIPs, ""]
        InstanceIDs: !GetAtt QSTACK.Outputs.ClusterInstanceIDs
        KeyName: !Ref KeyPair
        KeyPrefix: !Ref QSS3KeyPrefix
        MaxNodesDown: !If [ProvMultiAZ, !Ref QMaxNodesDown, "1"]
        ModOverness: !If [ProvMultiAZ, !Ref QModOverness, "NO"]
        Node1IP: !Select [0, !Split [", ", !GetAtt QSTACK.Outputs.ClusterPrivateIPs]]
        NodeIPs: !GetAtt QSTACK.Outputs.ClusterPrivateIPs
        NumberAZs: !If [ProvMultiAZ, !Ref NumberAZs, "1"]
        PrivateSubnetCidr: !Ref QSgCidr1
        PrivateSubnetId: !Select [0, !Ref PrivateSubnetIDs]
        QClusterName: !Ref QClusterName  
        QClusterVersion: !Ref QClusterVersion
        QPermissionsBoundary: !Ref QPermissionsBoundary        
        QStackName: !GetAtt QSTACK.Outputs.AWSStackName
        Region: !Ref AWS::Region
        RequireIMDSv2: !Ref RequireIMDSv2        
        SideCarProv: !Ref SideCarProv
        SideCarSecretsArn: !GetAtt SECRETSSTACK.Outputs.SideCarSecretsArn
        SoftwareSecretsArn: !GetAtt SECRETSSTACK.Outputs.SoftwareSecretsArn
        TermProtection: !Ref TermProtection
        TopStackName: !Ref TopStackName
        VPCID: !Ref VPCId
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/provisioning-node.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  CLOUDWATCHSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Properties:
      Parameters:
        AllFlash: !Select [1, !Split ["-", !If [Custom, !Ref QDiskConfig, !FindInMap [ConfigMap, !Ref QMarketPlaceType, DiskConfig]]]]
        QAuditLog: !Ref QAuditLog
        QClusterName: !Ref QClusterName        
        QNodeNames: !GetAtt QSTACK.Outputs.ClusterNodeNames
        QStackName: !GetAtt QSTACK.Outputs.AWSStackName
        Region: !Ref AWS::Region        
        TopStackName: !Ref TopStackName
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/cloud-watch.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  MGMTNLBSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Condition: ProvMgmt
    Properties:
      Parameters:
        EnableReplication: !Ref QPublicRepl
        NodeIPs: !GetAtt QSTACK.Outputs.ClusterPrivateIPs
        NumNodes: !If [CustomNC, !Ref QNodeCount, !FindInMap [ConfigMap, !Ref QMarketPlaceType, NodeCount]]
        PublicSubnetIDs: !Ref PublicSubnetIDs
        VPCID: !Ref VPCId
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/mgmt-nlb.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  QNLBSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Condition: ProvNLB
    Properties:
      Parameters:
        CrossZone: !Ref QNlbXzone
        DeregDelay: !Ref QNlbDeregDelay
        DeregTerm: !Ref QNlbDeregTerm
        NodeIPs: !GetAtt QSTACK.Outputs.ClusterPrivateIPs
        NumNodes: !If [CustomNC, !Ref QNodeCount, !FindInMap [ConfigMap, !Ref QMarketPlaceType, NodeCount]]
        PreserveIP: !Ref QNlbPreserveIP
        PrivateSubnetIDs: !Join [",", !Ref QNlbPrivateSubnetIDs]
        ProxyProtoV2: "false"
        Stickiness: !Ref QNlbSticky
        VPCID: !Ref VPCId
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/qnlb.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

  DNSSTACK:
    Type: 'AWS::CloudFormation::Stack'
    Condition: ProvR53
    Properties:
      Parameters:
        FQDName: !Ref DomainName
        FloatIPs: !GetAtt QSTACK.Outputs.ClusterSecondaryPrivateIPs
        NumIPs: !Ref QFloatingIP
        NumNodes: !If [CustomNC, !Ref QNodeCount, !FindInMap [ConfigMap, !Ref QMarketPlaceType, NodeCount]]
        RecordName: !Ref QFloatRecordName
        Region: !Ref AWS::Region
        VPCId: !Ref VPCId
      TemplateURL:
        !Sub
          - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/cfn/r53-private-zone.cft.yaml
          - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion]
            S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName]
      TimeoutInMinutes: 150   

Outputs:

  QumuloPrivateIP:
    Description: Private IP for Qumulo Cluster Management - Node 1
    Value: !GetAtt QSTACK.Outputs.LinkToManagement
  QumuloNLBPublicURL:
    Condition: ProvMgmt
    Description: Public URL for Management NLB connected to Qumulo Cluster
    Value: !GetAtt MGMTNLBSTACK.Outputs.NLBDNS
  QumuloPrivateNFS:
    Condition: ProvR53
    Description: Private NFS path for Qumulo Cluster
    Value: !GetAtt DNSSTACK.Outputs.ClusterNFS
  QumuloPrivateSMB:
    Condition: ProvR53
    Description: Private SMB UNC path for Qumulo Cluster
    Value: !GetAtt DNSSTACK.Outputs.ClusterSMB       
  QumuloPrivateURL:
    Condition: ProvR53
    Description: Private URL for Qumulo Cluster
    Value: !GetAtt DNSSTACK.Outputs.ClusterDNS     
  QumuloNLBPrivateNFS:
    Condition: ProvNLB
    Description: Private NFS path for Qumulo Cluster
    Value: !GetAtt QNLBSTACK.Outputs.NLBNFS    
  QumuloNLBPrivateSMB:
    Condition: ProvNLB
    Description: Private SMB UNC path for Qumulo Cluster
    Value: !GetAtt QNLBSTACK.Outputs.NLBSMB         
  QumuloNLBPrivateURL:
    Condition: ProvNLB
    Description: Private URL for NLB Connected to Qumulo Cluster
    Value: !GetAtt QNLBSTACK.Outputs.NLBDNS       
  QumuloKnowledgeBase:
    Description: Qumulo Knowledge Base
    Value: !GetAtt QSTACK.Outputs.QumuloKnowledgeBase