cloudamatic/mu

View on GitHub
modules/mu/cloud/machine_images.rb

Summary

Maintainability
A
1 hr
Test Coverage
# Copyright:: Copyright (c) 2020 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.

module MU
  # Plugins under this namespace serve as interfaces to cloud providers and
  # other provisioning layers.
  class Cloud

    # The public AWS S3 bucket where we expect to find YAML files listing our
    # standard base images for various platforms.
    BASE_IMAGE_BUCKET = "cloudamatic"
    # The path in the AWS S3 bucket where we expect to find YAML files listing
    # our standard base images for various platforms.
    BASE_IMAGE_PATH = "/images"

    # Aliases for platform names, in case we don't have actual images built for
    # them.
    PLATFORM_ALIASES = {
      "linux" => "centos7",
      "windows" => "win2k12r2",
      "win2k12" => "win2k12r2",
      "ubuntu" => "ubuntu16",
      "centos" => "centos7",
      "rhel7" => "rhel71",
      "rhel" => "rhel71",
      "amazon" => "amazon2016"
    }

    @@image_fetch_cache = {}
    @@platform_cache = []
    @@image_fetch_semaphore = Mutex.new

    # Rifle our image lists from {MU::Cloud.getStockImage} and return a list
    # of valid +platform+ names.
    # @return [Array<String>]
    def self.listPlatforms
      return @@platform_cache if @@platform_cache and !@@platform_cache.empty?
      @@platform_cache = MU::Cloud.supportedClouds.map { |cloud|
        begin
          resourceClass(cloud, :Server)
        rescue MU::Cloud::MuCloudResourceNotImplemented, MU::MuError
          next
        end

        images = MU::Cloud.getStockImage(cloud, quiet: true)
        if images
          images.keys
        else
          nil
        end
      }.flatten.uniq
      @@platform_cache.delete(nil)
      @@platform_cache.sort
      @@platform_cache
    end

    # Locate a base image for a {MU::Cloud::Server} resource. First we check
    # Mu's public bucket, which should list the latest and greatest. If we can't
    # fetch that, then we fall back to a YAML file that's bundled as part of Mu,
    # but which will typically be less up-to-date.
    # @param cloud [String]: The cloud provider for which to return an image list
    # @param platform [String]: The supported platform for which to return an image or images. If not specified, we'll return our entire library for the appropriate cloud provider.
    # @param region [String]: The region for which the returned image or images should be supported, for cloud providers which require it (such as AWS).
    # @param fail_hard [Boolean]: Raise an exception on most errors, such as an inability to reach our public listing, lack of matching images, etc.
    # @return [Hash,String,nil]
    def self.getStockImage(cloud = MU::Config.defaultCloud, platform: nil, region: nil, fail_hard: false, quiet: false)

      if !MU::Cloud.supportedClouds.include?(cloud)
        MU.log "'#{cloud}' is not a supported cloud provider! Available providers:", MU::ERR, details: MU::Cloud.supportedClouds
        raise MuError, "'#{cloud}' is not a supported cloud provider!"
      end

      urls = ["http://"+BASE_IMAGE_BUCKET+".s3-website-us-east-1.amazonaws.com"+BASE_IMAGE_PATH]
      if $MU_CFG and $MU_CFG['custom_images_url']
        urls << $MU_CFG['custom_images_url']
      end
      
      images = nil
      urls.each { |base_url|
        @@image_fetch_semaphore.synchronize {
          if @@image_fetch_cache[cloud] and (Time.now - @@image_fetch_cache[cloud]['time']) < 30
            images = @@image_fetch_cache[cloud]['contents'].dup
          else
            begin
              Timeout.timeout(2) do
                response = URI.open("#{base_url}/#{cloud}.yaml").read
                images ||= {}
                images.deep_merge!(YAML.load(response))
                break
              end
            rescue StandardError => e
              if fail_hard
                raise MuError, "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})"
              else
                MU.log "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})", MU::WARN if !quiet
              end
            end
          end
        }
      }

      @@image_fetch_semaphore.synchronize {
        @@image_fetch_cache[cloud] = {
          'contents' => images.dup,
          'time' => Time.now
        }
      }

      backwards_compat = {
        "AWS" => "amazon_images",
        "Google" => "google_images",
      }

      # Load from inside our repository, if we didn't get images elsewise
      if images.nil?
        [backwards_compat[cloud], cloud].each { |file|
          next if file.nil?
          if File.exist?("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml")
            images = YAML.load(File.read("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml"))
            break
          end
        }
      end

      # Now overlay local overrides, both of the systemwide (/opt/mu/etc) and
      # per-user (~/.mu/etc) variety.
      [backwards_compat[cloud], cloud].each { |file|
        next if file.nil?
        if File.exist?("#{MU.etcDir}/#{file}.yaml")
          images ||= {}
          images.deep_merge!(YAML.load(File.read("#{MU.etcDir}/#{file}.yaml")))
        end
        if Process.uid != 0
          basepath = Etc.getpwuid(Process.uid).dir+"/.mu/etc"
          if File.exist?("#{basepath}/#{file}.yaml")
            images ||= {}
            images.deep_merge!(YAML.load(File.read("#{basepath}/#{file}.yaml")))
          end
        end
      }

      if images.nil?
        if fail_hard
          raise MuError, "Failed to find any base images for #{cloud}"
        else
          MU.log "Failed to find any base images for #{cloud}", MU::WARN if !quiet
          return nil
        end
      end

      PLATFORM_ALIASES.each_pair { |a, t|
        if images[t] and !images[a]
          images[a] = images[t]
        end
      }

      if platform
        if !images[platform]
          if fail_hard
            raise MuError, "No base image for platform #{platform} in cloud #{cloud}"
          else
            MU.log "No base image for platform #{platform} in cloud #{cloud}", MU::WARN if !quiet
            return nil
          end
        end
        images = images[platform]

        if region
          # We won't fuss about the region argument if this isn't a cloud that
          # has regions, just quietly don't bother.
          if images.is_a?(Hash)
            if images[region]
              images = images[region]
            else
              if fail_hard
                raise MuError, "No base image for platform #{platform} in cloud #{cloud} region #{region} found"
              else
                MU.log "No base image for platform #{platform} in cloud #{cloud} region #{region} found", MU::WARN if !quiet
                return nil
              end
            end
          end
        end
      else
        if region
          images.values.each { |regions|
            # Filter to match our requested region, but for all the platforms,
            # since we didn't specify one.
            if regions.is_a?(Hash)
              regions.delete_if { |r| r != region }
            end
          }
        end
      end

      images
    end

  end

end