module OpenNebula::MarketPlaceAppExt

Module to decorate MarketApp class with additional helpers not directly exposed through the OpenNebula XMLRPC API. The extensions include

- export helper that creates OpenNebula related objects from a given app.

rubocop:disable Style/ClassAndModuleChildren

Public Class Methods

extend_object(obj) click to toggle source
Calls superclass method
# File lib/opennebula/marketplaceapp_ext.rb, line 26
    def self.extend_object(obj)
        if !obj.is_a?(OpenNebula::MarketPlaceApp)
            raise StandardError, "Cannot extended #{obj.class} " \
                                 'with MarketPlaceAppExt'
        end

        class << obj

            ####################################################################
            # Public extended interface
            ####################################################################
            # Exports this app to a suitable OpenNebula object
            # @param appid [Integer] id of the marketplace app
            # @param options [Hash] to control the export behavior
            #   dsid [Integer] datastore to save images
            #   f_dsid [Integer] file datastore to save kernels
            #   name [String] of the new object
            #   vmtemplate_name [String] name for the VM Template, if the App
            #   has one complex [Boolean] true to only create image
            #
            # @return [Hash, OpenNebula::Error] with the ID and type of
            # the created objects. Instead of an ID, the array may
            # contain OpenNebula::Error with specific object creation errors
            #   {
            #     :vm => [ vm ids/OpenNebula::Error ],
            #     :vmtemplate => [ vmtemplates ids/OpenNebula::Error ],
            #     :image => [ vm ids/OpenNebula::Error ]
            #   }
            #    rc_info = {
            #      :image            => [],
            #      :image_type       => nil,
            #      :vmtemplate       => [],
            #      :service_template => []
            #    }
            def export(options = {})
                # Get App information and check for errors
                rc = info
                return rc if OpenNebula.is_error?(rc)
                return Error.new('App is not READY') if state_str != 'READY'

                if options[:dsid].nil?
                    return Error.new('Missing datastore id')
                end

                if options[:name].nil?
                    return Error.new('Missing name to export app')
                end

                case type_str
                when 'IMAGE'
                    export_image(options)
                when 'VMTEMPLATE'
                    options[:notemplate] = true

                    export_vm_template(options)
                when 'SERVICE_TEMPLATE'
                    export_service_template(options)
                else
                    Error.new("App type #{type_str} not supported")
                end
            end

            ####################################################################
            # Private methods
            ####################################################################
            private

            # Exports an OpenNebula Image from this marketplace app
            #   @param options to export the image
            #     :vmtemplate_name [String] name of new image and template
            #     :dsid [String] Datastore id to create the image
            #     :f_dsid [String] Files Datastore id
            #     :notemplate [Bool] if true do not create vm_template (if any)
            #     :template [Integer] Template id to use with image
            #     :default_template [String] Default template id for vCenter
            #   @return [Hash]
            #     :image [Array] of the new image
            #     :image_type [String] of the new image (CONTEXT, KERNEL, CDROM)
            #     :vmtemplate [Array] of the associated vm_template (if any)
            #     :service_template [Array] empty []
            def export_image(options)
                rc_info = {
                    :image            => [],
                    :image_type       => nil,
                    :vmtemplate       => [],
                    :service_template => []
                }

                #---------------------------------------------------------------
                # Create the image template
                #---------------------------------------------------------------
                tmpl = ''

                if self['APPTEMPLATE64']
                    tmpl = Base64.decode64(self['APPTEMPLATE64'])
                end

                tmpl << <<-EOT
                NAME     = "#{options[:name]}"
                FROM_APP = "#{self['ID']}"
                EOT

                #---------------------------------------------------------------
                # Kernel or context images stored in a files datastore
                #---------------------------------------------------------------
                dsid = options[:dsid]

                if tmpl.match(/^\s*TYPE\s*=\s*KERNEL|CONTEXT\s/i)
                    if options[:f_dsid]
                        dsid = options[:f_dsid]
                    else
                        dspool = DatastorePool.new(@client)

                        rc = dspool.info
                        return { :image => [rc] } if OpenNebula.is_error?(rc)

                        file_ds = dspool.select {|d| d['TYPE'].to_i == 2 }
                        dsid = file_ds.first['ID'].to_i
                    end
                end

                ds = OpenNebula::Datastore.new_with_id(dsid, @client)
                rc = ds.info

                is_vcenter = !OpenNebula.is_error?(rc) &&
                             (ds['TEMPLATE/DRIVER'] == 'vcenter')

                if is_vcenter && options[:template].nil?
                    options = update_options_with_template(options)
                end

                #---------------------------------------------------------------
                # Allocate the image in OpenNebula
                #---------------------------------------------------------------
                image = Image.new(Image.build_xml, @client)
                rc    = image.allocate(tmpl, dsid)

                if OpenNebula.is_error?(rc)
                    rc_info[:image] = [rc]

                    return rc_info
                end

                image.info

                image.delete_element('TEMPLATE/FORMAT')
                image.delete_element('TEMPLATE/DRIVER')
                image.delete_element('TEMPLATE/DEV_PREFIX') if is_vcenter

                image.update(image.template_xml)

                rc_info[:image] = [image.id]
                rc_info[:image_type] = image.type_str

                #---------------------------------------------------------------
                # Created an associated VMTemplate if needed
                #---------------------------------------------------------------
                if is_vcenter &&
                    !options[:notemplate] &&
                    (!options[:template] || options[:template] == -1)
                    tmpl = create_vcenter_template(
                        ds,
                        options,
                        self['TEMPLATE/VMTEMPLATE64'],
                        image
                    )

                    if OpenNebula.is_error?(tmpl)
                        rc_info[:vmtemplate] = [tmpl]
                    else
                        rc_info[:vmtemplate] = [tmpl.id]
                    end

                    return rc_info
                end

                if self['TEMPLATE/VMTEMPLATE64'].nil? ||
                   options[:notemplate] ||
                   options[:template] == -1
                    return rc_info
                end

                if !options[:template].nil?
                    template_id = options[:template]

                    if template_id < 0
                        raise 'Invalid option, template_id must be a valid ID'
                    end

                    template = Template.new_with_id(template_id, @client)

                    vmtpl_id = template.clone(
                        options[:vmtemplate_name] || options[:name]
                    )

                    tmpl << <<-EOT

                    NAME = "#{options[:vmtemplate_name] || options[:name]}"
                    DISK = [ IMAGE_ID = "#{image.id}" ]
                    EOT

                    template = Template.new_with_id(vmtpl_id, @client)

                    template.update(tmpl, true)

                    rc_info[:vmtemplate] = [vmtpl_id]
                else
                    tmpl = Base64.decode64(self['TEMPLATE/VMTEMPLATE64'])
                    tmpl << <<-EOT

                    NAME = "#{options[:vmtemplate_name] || options[:name]}"
                    DISK = [ IMAGE_ID = "#{image.id}" ]
                    EOT

                    vmtpl = Template.new(Template.build_xml, @client)

                    rc = vmtpl.allocate(tmpl)
                    rc = vmtpl.id unless OpenNebula.is_error?(rc)

                    rc_info[:vmtemplate] = [rc]
                end

                rc_info
            end

            # Export complex template from marketplace
            #
            # @param name  [Hash]    Export options
            # @param ds_id [Integer] Datastore ID to export child apps
            def export_vm_template(options)
                rc = {
                    :image            => [],
                    :image_type       => nil,
                    :vmtemplate       => [],
                    :service_template => []
                }

                vmtmpl = export_recursive('//DISK', options) do |disks|
                    create_vm_template(options, disks)
                end

                rc[:vmtemplate] = [vmtmpl[0]]

                unless OpenNebula.is_error?(vmtmpl[0])
                    rc[:image] += vmtmpl[1]
                    rc[:vmtemplate] += vmtmpl[2] unless options[:notemplate]
                end

                rc
            end

            # Export service template from marketplace
            #
            # @param name    [Hash]    Export options
            # @param ds_id   [Integer] Datastore ID to export child apps
            def export_service_template(options)
                rc = {
                    :image            => [],
                    :image_type       => nil,
                    :vmtemplate       => [],
                    :service_template => []
                }

                stmpl = export_recursive('//ROLE', options) do |roles|
                    create_service_template(options, roles)
                end

                rc[:service_template] = [stmpl[0]]

                unless OpenNebula.is_error?(stmpl[0])
                    rc[:image] += stmpl[1]
                    rc[:vmtemplate] += stmpl[2]
                end

                rc
            end

            # Create a VM template in vCenter in order to use it when
            # deploying an app from the marketplace
            #
            # @param ds       [OpenNebula::Datastore] Datastore information
            # @param options  [Hash]                  Export options
            # @param template [String]                App template
            # @param image    [OpenNebula::Image]     Image information
            def create_vcenter_template(ds, options, template, image = nil)
                ret  = {}
                keys = ['VCENTER_TEMPLATE_REF',
                        'VCENTER_CCR_REF',
                        'VCENTER_INSTANCE_ID']

                if ds['//VCENTER_TEMPLATE_REF']
                    keys.each do |key|
                        ret[key] = ds["//#{key}"]
                    end
                else
                    require 'vcenter_driver'

                    # Get vi client for current datastore
                    vi_client = VCenterDriver::VIClient.new_from_datastore(
                        ds.id
                    )

                    # Get datastore object
                    ds_ref    = ds['//VCENTER_DS_REF']
                    datastore = VCenterDriver::Datastore.new_from_ref(
                        ds_ref,
                        vi_client
                    )

                    # Get resource pool
                    host_ref          = datastore['host'].first.key.parent._ref
                    vi_client.ccr_ref = host_ref

                    host = VCenterDriver::ClusterComputeResource.new_from_ref(
                        host_ref,
                        vi_client
                    )

                    rp = host.resource_pools.first

                    # Get vCentrer instance ID
                    uuid = vi_client.vim.serviceContent.about.instanceUuid

                    # Create VM folder it not exists
                    dc        = datastore.obtain_dc.item
                    vm_folder = dc.find_folder('one_default_template')

                    if vm_folder.nil?
                        dc.vmFolder.CreateFolder(
                            :name => 'one_default_template'
                        )
                        vm_folder = dc.find_folder('one_default_template')
                    end

                    # Define default VM config
                    vm_cfg = { :name     => "one_app_template-#{ds.id}",
                            :guestId  => 'otherGuest',
                            :numCPUs  => 1,
                            :memoryMB => 128,
                            :files    => {
                                :vmPathName => "[#{datastore.item.name}]"
                            } }

                    # Create the VM
                    vm = vm_folder.CreateVM_Task(
                        :config => vm_cfg,
                        :pool   => rp
                    ).wait_for_completion

                    # Create the VM template
                    vm.MarkAsTemplate

                    ret['VCENTER_TEMPLATE_REF'] = vm._ref
                    ret['VCENTER_CCR_REF']      = host_ref
                    ret['VCENTER_INSTANCE_ID']  = uuid

                    ret.each do |key, value|
                        ds.update("#{key}=\"#{value}\"", true)
                    end
                end

                tmpl = <<-EOT
                NAME = "#{options[:vmtemplate_name] || options[:name]}"
                HYPERVISOR = "vcenter"
                EOT

                tmpl << "DISK = [ IMAGE_ID = \"#{image.id}\" ]" if image

                template ||= ''
                template   = Base64.decode64(template)

                template.split("\n").each do |line|
                    # Legacy, some apps in the marketplace have the sched
                    # requirement to just be run on KVM, with this
                    # the template cannot be run on vCenter, so don't add
                    # it in the final VM template
                    next if line =~ /SCHED_REQUIREMENTS/ || line.empty?

                    tmpl << "#{line}\n"
                end

                ret.each do |key, value|
                    tmpl << "#{key}=\"#{value}\"\n"
                end

                vmtpl = Template.new(Template.build_xml, @client)

                rc = vmtpl.allocate(tmpl)

                if OpenNebula.is_error?(rc)
                    rc
                else
                    Template.new_with_id(vmtpl.id, @client)
                end
            end

            def update_options_with_template(options)
                path = "#{VAR_LOCATION}/remotes/etc/vmm/vcenter/vcenterrc"

                return options unless File.file?(path)

                config = YAML.load_file(path)

                return options unless config.key?(:default_template)

                options[:template] = config[:default_template]

                options
            end

            # Creates a VM template based on the APPTEMPLATE64 attribute
            #    @param [Hash] options
            #       :export_name [String] name of the vm template
            #    @param [Hash] disks exported disks from related apps.
            #    As returned by the export_image method
            #
            #    @return [Integer, OpenNebula::Error] template id or error
            def create_vm_template(options, disks)
                dsid = options[:dsid]
                ds = OpenNebula::Datastore.new_with_id(dsid, @client)
                rc = ds.info

                is_vcenter = !OpenNebula.is_error?(rc) && ds['TEMPLATE/DRIVER'] == 'vcenter'

                if is_vcenter
                    if options[:template].nil?
                        options = update_options_with_template(options)
                    end

                    if !options[:template] || options[:template] == -1
                        vmtpl = create_vcenter_template(
                            ds,
                            options,
                            self['TEMPLATE/VMTEMPLATE64']
                        )
                    else
                        template_id = options[:template]
                        template    = Template.new_with_id(template_id, @client)

                        vmtpl_id = template.clone(
                            options[:vmtemplate_name] || options[:name]
                        )

                        vmtpl = Template.new_with_id(vmtpl_id, @client)
                    end

                    rc = vmtpl.info
                else
                    # ----------------------------------------------------------
                    # Allocate Template
                    # ----------------------------------------------------------
                    if self['TEMPLATE/APPTEMPLATE64'].nil?
                        return Error.new(
                            "Missing APPTEMPLATE64 for App #{id}"
                        )
                    end

                    tmpl = Base64.decode64(self['TEMPLATE/APPTEMPLATE64'])

                    tmpl << "\nNAME=\"#{options[:name]}\"\n"

                    vmtpl = Template.new(Template.build_xml, @client)
                    rc    = vmtpl.allocate(tmpl)
                end

                return rc if OpenNebula.is_error?(rc)

                # --------------------------------------------------------------
                # Update disk information in template
                # --------------------------------------------------------------
                vmtpl.info

                context = []

                disks.each do |_app, disk|
                    id = disk[:image]

                    case disk[:image_type]
                    when 'IMAGE', 'OS', 'DATABLOCK', 'CDROM'
                        vmtpl.add_element('TEMPLATE',
                                          'DISK' => { 'IMAGE_ID' => id.first })
                    when 'CONTEXT'
                        context << "$FILE[IMAGE_ID=\"#{id}\"]"
                    when 'KERNEL'
                        if !vmtpl.has_elements?('TEMPLATE/OS')
                            vmtpl.add_element('TEMPLATE', 'OS'=>{})
                        end

                        vmtpl.add_element(
                            'TEMPLATE/OS',
                            'KERNEL_DS' => "$FILE[IMAGE_ID=#{id}]"
                        )
                    end
                end

                if !context.empty?
                    if !vmtpl.has_elements?('TEMPLATE/CONTEXT')
                        vmtpl.add_element('TEMPLATE', 'CONTEXT' => {})
                    end

                    vmtpl.add_element('TEMPLATE/CONTEXT',
                                      'FILES_DS' => context.join(' '))
                end

                # --------------------------------------------------------------
                # Update template information in OpenNebula
                # --------------------------------------------------------------
                vmtpl.update(vmtpl.template_xml)

                vmtpl.id
            end

            # Creates a Service template based on the VMTEMPLATE64 attribute
            # @return [Integer, OpenNebula::Error] template id or error
            def create_service_template(options, roles)
                # --------------------------------------------------------------
                # Allocate Template
                # --------------------------------------------------------------
                if self['TEMPLATE/APPTEMPLATE64'].nil?
                    return Error.new("Missing APPTEMPLATE64 for App #{id}")
                end

                tmpl = Base64.decode64(self['TEMPLATE/APPTEMPLATE64'])
                tmpl = JSON.parse(tmpl)

                tmpl['name'] = options[:name]

                # --------------------------------------------------------------
                # Append template IDs to each role information
                # --------------------------------------------------------------
                tmpl['roles'].each do |role|
                    t_id = roles.find {|_, v| v[:names].include?(role['name']) }

                    if t_id.nil? || t_id[1].nil? || t_id[1][:vmtemplate].nil?
                        next
                    end

                    role['vm_template'] = nil
                    role['vm_template'] = t_id[1][:vmtemplate][0]
                end

                # --------------------------------------------------------------
                # Allocate Service template in OpenNebula
                # --------------------------------------------------------------
                stmpl = ServiceTemplate.new(ServiceTemplate.build_xml, @client)

                rc = stmpl.allocate(tmpl.to_json)
                rc = stmpl.id unless OpenNebula.is_error?(rc)

                rc
            end

            # Export complex template from marketplace
            #
            # @param xpath   [String]  Xpath to search childs
            # @param options [Hash]    Export options
            # @param ds_id   [Integer] Datastore ID to export child apps
            def export_recursive(xpath, options)
                # Get marketplace apps pool to find roles apps
                pool = OpenNebula::MarketPlaceAppPool.new(@client)
                rc   = pool.info_all

                return [rc, [], []] if OpenNebula.is_error?(rc)

                # Apps that have been already exported
                #
                # app_name =>
                #   :vmtempalte = exported template ID
                #   :image      = exported image ID
                #   :image_type = type (KERNEL, CONTEXT...) of image
                #   :names      = [name_a, name_b, ...]
                exported = {}
                idx      = 0
                idy      = 0
                opt_name = ''

                # Store IDs of created resources
                images    = []
                templates = []

                # Iterate over all childs
                rc = retrieve_xmlelements(xpath).each do |obj|
                    # Get name and app information
                    obj_name = obj['NAME']
                    app      = obj['APP']

                    # If the app was already exported, do not export it again
                    if exported[app]
                        exported[app][:names] << obj_name
                        next
                    end

                    # Find app in pool
                    obj = pool.find {|p| p['NAME'] == app }

                    break Error.new("App `#{app}` not found") unless obj

                    obj.extend(MarketPlaceAppExt)

                    # Fix name if duplicates exist
                    imgp = OpenNebula::ImagePool.new(@client)
                    rc = imgp.info
                    break rc if OpenNebula.is_error?(rc)

                    img_names = imgp.retrieve_elements('/IMAGE_POOL/IMAGE/NAME')

                    opt_name = options[:name]
                    t_short = "#{opt_name}-#{obj_name}-#{idx}"

                    if !img_names.nil? && img_names.include?(t_short)
                        idy = 0
                        while img_names.include? \
                            "#{opt_name}_#{idy}-#{obj_name}-#{idx}"
                            idy += 1
                        end
                        opt_name = "#{opt_name}_#{idy}"
                    end

                    rc = obj.export(
                        :dsid       => options[:dsid],
                        :name       => "#{opt_name}-#{obj_name}-#{idx}",
                        :notemplate => options[:notemplate]
                    )

                    image = rc[:image].first if rc[:image]
                    break image if OpenNebula.is_error?(image)

                    vmtemplate = rc[:vmtemplate].first if rc[:vmtemplate]
                    break vmtemplate if OpenNebula.is_error?(vmtemplate)

                    idx += 1

                    # Update exported hash with information
                    exported[app] = rc
                    exported[app][:names] = [obj_name]

                    # Add IDs to return object
                    images << image
                    templates << vmtemplate
                end

                if block_given? && !OpenNebula.is_error?(rc)
                    rc = yield(exported)
                end

                if OpenNebula.is_error?(rc)
                    rollback_export(exported, xpath != '//DISK')
                end

                [rc, images, templates]
            end

            # Delete templates/images in case something went wrong
            #
            # @param exported [Hash]    Exported apps information
            # @param template [Boolean] True to delete VM Template
            #                           False to delete images
            #
            # @return [nil | OpenNebula::Error]
            def rollback_export(exported, is_template)
                ret = ''

                if is_template
                    obj_factory = lambda {|v|
                        id = v[:vmtemplate].first
                        [Template.new_with_id(id, @client),
                         "Error deleting template #{id}"]
                    }

                    delete_method = 'delete'
                    args          = true
                else
                    obj_factory = lambda {|v|
                        id = v[:image].first
                        [Image.new_with_id(id, @client),
                         "Error deleting image #{id}"]
                    }

                    delete_method = 'delete'
                end

                exported.each do |_, v|
                    obj, err_msg = obj_factory.call(v)

                    if args
                        rc = obj.send(delete_method, args)
                    else
                        rc = obj.send(delete_method)
                    end

                    next unless OpenNebula.is_error?(rc)

                    ret << err_msg
                end

                if ret.empty?
                    nil
                else
                    Error.new(ret) unless ret.empty?
                end
            end

        end

        super
    end