cloudamatic/mu

View on GitHub
modules/mu/providers/cloudformation/database.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# Copyright:: Copyright (c) 2016 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#    http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

autoload :Net, 'net/ssh/gateway'

module MU
  class Cloud
    class CloudFormation
      # A database as configured in {MU::Config::BasketofKittens::databases}
      class Database < MU::Cloud::Database
        @deploy = nil
        @config = nil
        attr_reader :mu_name
        attr_reader :cloud_id
        attr_reader :config
        attr_reader :groomer    

        attr_reader :cfm_name
        attr_reader :cfm_template

        # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
        # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::databases}
        def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
          @deploy = mommacat
          @config = kitten_cfg
          @cloud_id ||= cloud_id
          # @mu_name = mu_name ? mu_name : @deploy.getResourceName(@config["name"])
          @config["groomer"] = MU::Config.defaultGroomer unless @config["groomer"]
          @groomclass = MU::Groomer.loadGroomer(@config["groomer"])

          if !mu_name.nil?
            @mu_name = mu_name
          elsif @config['scrub_mu_isms']
            @mu_name = @config['name']
          else
            @mu_name ||=
              if @config["engine"].match(/^sqlserver/)
                @deploy.getResourceName(@config["name"], max_length: 15)
              else
                @deploy.getResourceName(@config["name"], max_length: 63)
              end

            @mu_name.gsub(/(--|-$)/i, "").gsub(/(_)/, "-").gsub!(/^[^a-z]/i, "")
          end
        end

        # Populate @cfm_template with a resource description for this database
        # in CloudFormation language.
        def create
          # RDS is picky, we can't just use our regular node names for things
          # like the default schema or username. And it varies from engine to
          # engine.
          basename = @config["name"].to_s
          basename = basename+@deploy.timestamp+MU.seed.downcase if !@config['scrub_mu_isms']
          basename.gsub!(/[^a-z0-9]/i, "")
          @config["db_name"] = MU::Cloud.resourceClass("AWS", "Database").getName(basename, type: "dbname", config: @config)
          @config['master_user'] = MU::Cloud.resourceClass("AWS", "Database").getName(basename, type: "dbuser", config: @config)

          if @config["create_cluster"]
            @cfm_name, @cfm_template = MU::Cloud::CloudFormation.cloudFormationBase("dbcluster", self, tags: @config['tags'], scrub_mu_isms: @config['scrub_mu_isms']) if @cfm_template.nil?
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "Port", @config['port']) if @config['port']
          else
            @cfm_name, @cfm_template = MU::Cloud::CloudFormation.cloudFormationBase(self.class.cfg_name, self, tags: @config['tags'], scrub_mu_isms: @config['scrub_mu_isms']) if @cfm_template.nil?
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBInstanceClass", @config['size'])
            if !@config['storage'].nil?
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "AllocatedStorage", @config['storage'].to_s)
            end


            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBInstanceIdentifier", @config['db_name']) if @config['db_name']
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "PubliclyAccessible", @config['publicly_accessible'])
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "Iops", @config['iops']) if @config['iops']

            ["allow_major_version_upgrade", "auto_minor_version_upgrade", "license_model", "storage_type", "port"].each { |arg|
              if !@config[arg].nil?
                key = ""
                arg.split(/_/).each { |chunk| key = key + chunk.capitalize }
                MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], key, @config[arg])
              end
            }

            if @config['multi_az_on_create'] or @config['multi_az_on_deploy']
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "MultiAZ", true)
            end

            if @config['read_replica_of'] and !@config["add_cluster_node"]
              rr = @config['read_replica_of']
              if rr['db_name']
                if @dependencies.has_key?("database") and @dependencies["database"].has_key?(rr['db_name'])
                MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "SourceDBInstanceIdentifier", { "Ref" => @dependencies["database"][rr['db_name']].cloudobj.cfm_name } )
                else
                  raise MuError, "Couldn't find database by name in read_replica_of stanza of #{@mu_name} (#{@config['read_replica_of']})"
                end
              elsif rr['db_id']
                MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "SourceDBInstanceIdentifier", rr['db_id'])
              end
            end

            # Are we supposed to be a cluster member?
            if @config["add_cluster_node"]
              cluster = nil
              rr = @config["member_of_cluster"]
              if rr['db_name']
                cluster = @deploy.findLitterMate(type: "database", name: rr['db_name'])
                MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBClusterIdentifier", { "Ref" => cluster.cloudobj.cfm_name })
              elsif rr["db_id"]
                MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBClusterIdentifier", rr["db_id"])
              else
                raise MuError, "Cannot resolve Database cluster id from #{rr}"
              end
              # Remove arguments that are invalid in clusters
              ["AllocatedStorage", "CharacterSetName", "DBSecurityGroups", "SourceDBInstanceIdentifier", "StorageType"].each { |arg|
                @cfm_template[@cfm_name]["Properties"].delete(arg)
              }
# XXX @config["subnet_group_name"] = @config['cluster_identifier']
            end
          end

          # Parameter groups- more or less common between clusters and instances
          if @config['parameter_group_family']
            params_name = params_template = nil
            if @config["create_cluster"]
              params_name, params_template = MU::Cloud::CloudFormation.cloudFormationBase("dbclusterparametergroup", name: @mu_name, tags: @config['tags'], scrub_mu_isms: @config['scrub_mu_isms'])
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBClusterParameterGroupName", { "Ref" => params_name })
            else
              params_name, params_template = MU::Cloud::CloudFormation.cloudFormationBase("dbparametergroup", name: @mu_name, tags: @config['tags'], scrub_mu_isms: @config['scrub_mu_isms'])
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBParameterGroupName", { "Ref" => params_name })
            end
            MU::Cloud::CloudFormation.setCloudFormationProp(params_template[params_name], "Description", "Parameter group for database #{@mu_name}")
            MU::Cloud::CloudFormation.setCloudFormationProp(params_template[params_name], "Family", @config['parameter_group_family'])
            if @config["db_parameter_group_parameters"] && !@config["db_parameter_group_parameters"].empty?
              params = {}
              @config["db_parameter_group_parameters"].each { |item|
                params[item['name']] = item['value']
              }
              MU::Cloud::CloudFormation.setCloudFormationProp(params_template[params_name], "Parameters", params)
            end

            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DependsOn", params_name)
            @cfm_template.merge!(params_template)
          end

          # DB Subnet groups also common between clusters and instances
          if @config["vpc"]
            subnets_name, subnets_template = MU::Cloud::CloudFormation.cloudFormationBase("dbsubnetgroup", name: @mu_name, tags: @config['tags'], scrub_mu_isms: @config['scrub_mu_isms'])
            MU::Cloud::CloudFormation.setCloudFormationProp(subnets_template[subnets_name], "DBSubnetGroupDescription", @mu_name)
            if !@config["vpc"]["subnets"].nil? and @config["vpc"]["subnets"].size > 0
              @config["vpc"]["subnets"].each { |subnet|
                if !subnet["subnet_id"].nil?
                  MU::Cloud::CloudFormation.setCloudFormationProp(subnets_template[subnets_name], "SubnetIds", subnet["subnet_id"])
                elsif @dependencies.has_key?("vpc") and @dependencies["vpc"].has_key?(@config["vpc"]["vpc_name"])
                  @dependencies["vpc"][@config["vpc"]["vpc_name"]].subnets.each { |subnet_obj|
                    if subnet_obj.name == subnet['subnet_name']
                      MU::Cloud::CloudFormation.setCloudFormationProp(subnets_template[subnets_name], "DependsOn", subnet_obj.cfm_name)
                      MU::Cloud::CloudFormation.setCloudFormationProp(subnets_template[subnets_name], "SubnetIds", { "Ref" => subnet_obj.cfm_name } )
                    end
                  }
                end
              }
            end
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBSubnetGroupName", { "Ref" => subnets_name } )
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DependsOn", subnets_name)

            if @config['add_firewall_rules']
              @config['add_firewall_rules'].each { |acl|
                if acl["rule_id"]
                  MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "VPCSecurityGroups", acl["rule_id"])
                else
                  MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "VPCSecurityGroups", { "Ref" => @dependencies["firewall_rule"][acl["rule_name"]].cloudobj.cfm_name })
                end
              }
            end

            @cfm_template.merge!(subnets_template)
          end

          # Other common parameters
          MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "StorageEncrypted", @config['storage_encrypted'])
          MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "MasterUsername", @config['master_user'])
          ["engine", "backup_retention_period", "preferred_backup_window", "engine_version", "preferred_maintenance_window"].each { |arg|
            if !@config[arg].nil?
              key = ""
              arg.split(/_/).each { |chunk| key = key + chunk.capitalize }
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], key, @config[arg])
            end
          }

          # Source snapshots and passwords work almost the same
          if @config["creation_style"] == "new_snapshot"
            raise MuCloudFlagNotImplemented, "Database creation node 'new_snapshot' is not supported for CloudFormation targets"
          elsif @config["creation_style"] == "existing_snapshot"
            if !@config["create_cluster"]
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBSnapshotIdentifier", @config['identifier'])
            elsif !@config['read_replica_of']
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "SnapshotIdentifier", @config['identifier'])
            end
          else
            # This password will be stored in plain text somewhere. Probably
            # best off making it a parameter in most use cases, because whoa
            # nelly is that insecure
            if @config["create_cluster"]
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DatabaseName", @config['db_name'])
            elsif @config['db_name']
              MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "DBName", @config['db_name'])
            end
            getPassword
            MU::Cloud::CloudFormation.setCloudFormationProp(@cfm_template[@cfm_name], "MasterUserPassword", @config['password'])
          end
        end

        # Placeholder. This is a NOOP for CloudFormation, which doesn't build
        # resources directly.
        def groom
        end
        # Placeholder. This is a NOOP for CloudFormation, which doesn't build
        # resources directly.
        def allowHost
        end
        # Placeholder. This is a NOOP for CloudFormation, which doesn't build
        # resources directly.
        def self.find(*args)
          MU.log "find() not implemented for CloudFormation layer", MU::DEBUG
          nil
        end
        # Placeholder. This is a NOOP for CloudFormation, which doesn't build
        # resources directly.
        def self.cleanup(*args)
          MU.log "cleanup() not implemented for CloudFormation layer", MU::DEBUG
          nil
        end
        # Placeholder. This is a NOOP for CloudFormation, which doesn't build
        # resources directly.
        def notify
          {}
        end

        # Cloud-specific configuration properties.
        # @param config [MU::Config]: The calling MU::Config object
        # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
        def self.schema(config)
          MU::Cloud.resourceClass("AWS", "Database").schema(config)
        end

        # Cloud-specific pre-processing of {MU::Config::BasketofKittens::servers}, bare and unvalidated.
        # @param server [Hash]: The resource to process and validate
        # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
        # @return [Boolean]: True if validation succeeded, False otherwise
        def self.validateConfig(server, configurator)
          MU::Cloud.resourceClass("AWS", "Database").validateConfig(server, configurator)
        end

        # Does this resource type exist as a global (cloud-wide) artifact, or
        # is it localized to a region/zone?
        # @return [Boolean]
        def self.isGlobal?
          MU::Cloud.resourceClass("AWS", "Database").isGlobal?
        end


      end #class
    end #class
  end
end #module