class OpenNebula::ServiceTemplate

Service Template

Constants

DOCUMENT_TYPE
IMMUTABLE_ATTRS

List of attributes that can't be changed in update operation

registration_time: this is internal info managed by OneFlow server

SCHEMA
VM_ROLE_SCHEMA
VR_ROLE_SCHEMA

Public Class Methods

convert_template(body) click to toggle source

Compatibility function to translate a service template from the old format to the new one with vRouters with other data model changes

@param template [String] Hash body of the service template to translate

# File lib/opennebula/flow/service_template.rb, line 808
def self.convert_template(body)
    body['roles'].each do |role|
        # Mandatory
        role['template_id'] = role['vm_template'].to_i
        role['type']        = 'vm'

        # Optional attributes
        role['user_inputs']        = role['custom_attrs'] if role['custom_attrs']
        body['user_inputs_values'] = role['custom_attrs_values'] \
                                     if body['custom_attrs_values']

        # Change name and type (string -> object)
        role['template_contents'] = ServiceTemplate.upgrade_template_contents(
            role['vm_template_contents']
        ) if role['vm_template_contents']

        # Remove old attributes
        role.delete('vm_template')
        role.delete('vm_template_contents') if role['vm_template_contents']
        role.delete('custom_attrs') if role['custom_attrs']
        role.delete('custom_attrs_values') if role['custom_attrs_values']
    end

    body['user_inputs'] = body['custom_attrs'] if body['custom_attrs']
    body['user_inputs_values'] = body['custom_attrs_values'] if body['custom_attrs_values']

    body.delete('custom_attrs') if body['custom_attrs']
    body.delete('custom_attrs_values') if body['custom_attrs_values']
rescue StandardError => e
    return OpenNebula::Error.new(
        'An old service template format (OpenNebula v6.x) was detected and could not be ' \
        'automatically converted. Update it manually to the new format. Error: ' + e.message
    )
end
init_default_vn_name_template(vn_name_template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 312
def self.init_default_vn_name_template(vn_name_template)
    # rubocop:disable Style/ClassVars
    @@vn_name_template = vn_name_template
    # rubocop:enable Style/ClassVars
end
old_format?(body) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 798
def self.old_format?(body)
    body.key?('roles') && body['roles'].all? do |role|
        role.key?('vm_template') || role.key?('vm_template_contents')
    end
end
upgrade_template_contents(vm_template_contents) click to toggle source

Converts a RAW string in the form KEY = VAL to a hash

@param template [String] Raw string content in the form KEY = VAL,

representing vm_template_contents

@return [Hash, OpenNebula::Error] Hash representation of the raw content,

or an OpenNebula Error if the conversion fails
# File lib/opennebula/flow/service_template.rb, line 849
def self.upgrade_template_contents(vm_template_contents)
    return {} if vm_template_contents.nil? || vm_template_contents.empty?

    result = {}

    vm_template_contents.split(/\n(?![^\[]*\])/).each do |line|
        next unless line.include?('=')

        key, value = line.split('=', 2).map(&:strip)
        value = value.tr('"', '')

        # If the value is an array (e.g., NIC = [ ... ]),
        # process the content inside the brackets
        if value.start_with?('[') && value.end_with?(']')
            # Split the elements inside the brackets by commas
            array_elements = value[1..-2].split(/\s*,\s*/)

            # Create a hash combining all key-value pairs in the array
            array_result = {}
            array_elements.each do |element|
                sub_key, sub_value = element.split('=', 2).map(&:strip)
                array_result[sub_key] = sub_value
            end

            value = [array_result]
        else
            value = [value]
        end

        # If the key already exists in the result hash, add the new value
        if result[key]
            if result[key].is_a?(Array)
                result[key] << value.first
            else
                result[key] = [result[key], value.first]
            end
        else
            result[key] = value.first
        end
    end

    result.each do |key, val|
        if val.is_a?(Hash)
            result[key] = [val]
        end
    end

    result
end
validate(template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 530
def self.validate(template)
    # Compability mode v<7.0
    rc = ServiceTemplate.convert_template(template) \
        if ServiceTemplate.old_format?(template)

    raise Validator::ParseException(rc.message) if OpenNebula.is_error?(rc)

    validator = Validator::Validator.new(
        :default_values => true,
        :delete_extra_properties => false,
        :allow_extra_properties => true
    )

    validator.validate!(template, SCHEMA)

    template['roles'].each do |role|
        validate_role(role)
    end

    validate_values(template)
end
validate_role(template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 552
def self.validate_role(template)
    validator = Validator::Validator.new(
        :default_values => true,
        :delete_extra_properties => false,
        :allow_extra_properties => true
    )

    tmplt_type = template.fetch('type', 'vm')

    case tmplt_type
    when 'vm'
        validator.validate!(template, VM_ROLE_SCHEMA)
    when 'vr'
        validator.validate!(template, VR_ROLE_SCHEMA)
    else
        raise Validator::ParseException, "Unsupported role type \"#{template['type']}\""
    end
end
validate_values(template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 600
def self.validate_values(template)
    roles = template['roles']

    roles.each_with_index do |role, role_index|
        # General verification (applies to all roles)
        roles[(role_index+1)..-1].each do |other_role|
            if role['name'] == other_role['name']
                raise Validator::ParseException,
                      "Role name '#{role['name']}' is repeated"
            end
        end

        # Specific values verification per role type
        case role['type']
        when 'vm'
            parser = ElasticityGrammarParser.new
            validate_vmvalues(role, parser)
        when 'vr'
            validate_vrvalues(role)
        else
            raise Validator::ParseException,
                  "Unsupported role type \"#{template['type']}\""
        end
    end
end
validate_vmvalues(vmrole, parser) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 665
def self.validate_vmvalues(vmrole, parser)
    if !vmrole['min_vms'].nil? &&
        vmrole['min_vms'].to_i > vmrole['cardinality'].to_i

        raise Validator::ParseException,
              "Role '#{vmrole['name']}' 'cardinality' must be " \
              "greater than or equal to 'min_vms'"
    end

    if !vmrole['max_vms'].nil? &&
        vmrole['max_vms'].to_i < vmrole['cardinality'].to_i

        raise Validator::ParseException,
              "Role '#{vmrole['name']}' 'cardinality' must be " \
              "lower than or equal to 'max_vms'"
    end

    if ((vmrole['elasticity_policies'] &&
       !vmrole['elasticity_policies'].empty?) ||
       (vmrole['scheduled_policies'] &&
       !vmrole['scheduled_policies'].empty?)) &&
       (vmrole['min_vms'].nil? || vmrole['max_vms'].nil?)
        raise Validator::ParseException,
              "Role '#{vmrole['name']}' with " \
              " 'elasticity_policies' or " \
              "'scheduled_policies'must define both 'min_vms'" \
              " and 'max_vms'"
    end

    if vmrole['elasticity_policies']
        vmrole['elasticity_policies'].each_with_index do |policy, index|
            exp = policy['expression']

            if exp.empty?
                raise Validator::ParseException,
                      "Role '#{vmrole['name']}', elasticity policy " \
                      "##{index} 'expression' cannot be empty"
            end

            treetop = parser.parse(exp)
            next unless treetop.nil?

            raise Validator::ParseException,
                  "Role '#{vmrole['name']}', elasticity policy " \
                  "##{index} 'expression' parse error: " \
                  "#{parser.failure_reason}"
        end
    end

    return unless vmrole['scheduled_policies']

    vmrole['scheduled_policies'].each_with_index do |policy, index|
        start_time = policy['start_time']
        recurrence = policy['recurrence']

        if !start_time.nil?
            if !policy['recurrence'].nil?
                raise Validator::ParseException,
                      "Role '#{vmrole['name']}', scheduled policy "\
                      "##{index} must define "\
                      "'start_time' or 'recurrence', but not both"
            end

            begin
                next if start_time.match(/^\d+$/)

                Time.parse(start_time)
            rescue ArgumentError
                raise Validator::ParseException,
                      "Role '#{vmrole['name']}', scheduled policy " \
                      "##{index} 'start_time' is not a valid " \
                      'Time. Try with YYYY-MM-DD hh:mm:ss or ' \
                      '0YYY-MM-DDThh:mm:ssZ'
            end
        elsif !recurrence.nil?
            begin
                cron_parser = CronParser.new(recurrence)
                cron_parser.next
            rescue StandardError
                raise Validator::ParseException,
                      "Role '#{vmrole['name']}', scheduled policy " \
                      "##{index} 'recurrence' is not a valid " \
                      'cron expression'
            end
        else
            raise Validator::ParseException,
                  "Role '#{vmrole['name']}', scheduled policy #" \
                  "#{index} needs to define either " \
                  "'start_time' or 'recurrence'"
        end
    end
end
validate_vrvalues(vrrole) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 644
def self.validate_vrvalues(vrrole)
    nic_array   = vrrole.dig('template_contents', 'NIC')
    cardinality = vrrole['cardinality']

    return if nic_array.nil? || !nic_array.is_a?(Array)

    contains_floating_key = nic_array.any? do |nic|
        nic.keys.any? do |key|
            key.to_s.start_with?('FLOATING')
        end
    end

    return unless cardinality > 1 && !contains_floating_key

    raise(
        Validator::ParseException,
        "Role '#{vrrole['name']}' with 'cardinality' greather " \
        'than one must define a floating IP'
    )
end

Public Instance Methods

allocate(template_json) click to toggle source
Calls superclass method OpenNebula::DocumentJSON#allocate
# File lib/opennebula/flow/service_template.rb, line 320
def allocate(template_json)
    template = JSON.parse(template_json)
    ServiceTemplate.validate(template)

    template['registration_time'] = Integer(Time.now)

    super(template.to_json, template['name'])
end
clone(name) click to toggle source

Clones a service template

@param name [Stirng] New name

@return [Integer] New template ID

Calls superclass method OpenNebula::Document#clone
# File lib/opennebula/flow/service_template.rb, line 498
def clone(name)
    new_id = super

    doc = OpenNebula::ServiceTemplate.new_with_id(new_id, @client)
    rc  = doc.info

    return rc if OpenNebula.is_error?(rc)

    body = JSON.parse(doc["TEMPLATE/#{TEMPLATE_TAG}"])

    # add registration time, as the template is new
    body['registration_time'] = Integer(Time.now)

    # update the template with the new body
    DocumentJSON.instance_method(:update).bind(doc).call(body.to_json)

    # return the new document ID
    new_id
end
clone_recursively(name, mode) click to toggle source

Clone service template and the VM templates asssociated to it

@param name [String] New template name @param mode [Symbol] Cloning mode (:all, :templates)

@return [Integer] New document ID

# File lib/opennebula/flow/service_template.rb, line 420
def clone_recursively(name, mode)
    recursive = mode == 'all'

    # clone the document to get new ID
    new_id = clone(name)

    return new_id if OpenNebula.is_error?(new_id)

    doc = OpenNebula::ServiceTemplate.new_with_id(new_id, @client)
    rc  = doc.info

    return rc if OpenNebula.is_error?(rc)

    body             = JSON.parse(doc["TEMPLATE/#{TEMPLATE_TAG}"])
    cloned_templates = {}

    # iterate over roles to clone templates
    rc = body['roles'].each do |role|
        t_id = role['template_id']

        # if the template has already been cloned, just update the value
        if cloned_templates.keys.include?(t_id)
            role['template_id'] = cloned_templates[t_id]
            next
        end

        template = OpenNebula::Template.new_with_id(t_id, @client)
        rc       = template.info

        break rc if OpenNebula.is_error?(rc)

        # The maximum size is 128, so crop the template name if it
        # exceeds the limit
        new_name = "#{template.name}-#{name}"

        if new_name.size > 119
            new_name = "#{template.name[0..(119 - name.size)]}-#{name}"
        end

        rc = template.clone(new_name, recursive)

        break rc if OpenNebula.is_error?(rc)

        # add new ID to the hash
        cloned_templates[t_id] = rc

        role['template_id'] = rc
    end

    # if any error, rollback and delete the left templates
    if OpenNebula.is_error?(rc)
        cloned_templates.each do |_, value|
            template = OpenNebula::Template.new_with_id(value, @client)

            rc = template.info

            break rc if OpenNebula.is_error?(rc)

            rc = template.delete(recursive)

            break rc if OpenNebula.is_error?(rc)
        end

        return rc
    end

    # update the template with the new body
    doc.update(body.to_json)

    # return the new document ID
    new_id
end
delete(type = nil) click to toggle source

Delete service template

@param type [String] Delete type

- none: just the service template
- all: delete VM templates, images and service template
- templates: delete VM templates and service template
Calls superclass method OpenNebula::Document#delete
# File lib/opennebula/flow/service_template.rb, line 335
def delete(type = nil)
    case type
    when 'all'
        recursive = true
    when 'templates'
        recursive = false
    end

    if type && type != 'none'
        rc = vm_template_ids

        return rc if OpenNebula.is_error?(rc)

        rc = rc.each do |t_id|
            t  = OpenNebula::Template.new_with_id(t_id, @client)
            rc = t.info

            break rc if OpenNebula.is_error?(rc)

            rc = t.delete(recursive)

            break rc if OpenNebula.is_error?(rc)
        end
    end

    return rc if OpenNebula.is_error?(rc)

    super()
end
handle_nested_values(template, extra_template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 758
def handle_nested_values(template, extra_template)
    roles        = template['roles']
    extra_roles  = extra_template.fetch('roles', [])

    # Check if nics key already exist
    template_nets = template['networks_values'] || []
    extra_nets    = extra_template['networks_values'] || []

    unless extra_nets.empty?
        extra_nets.each do |extra_net|
            next unless extra_net.is_a?(Hash) && !extra_net.empty?

            net_name  = extra_net.keys.first
            net_index = template_nets.index {|net| net.key?(net_name) }

            if net_index
                template_nets[net_index] = extra_net
            else
                template_nets << extra_net
            end
        end

        template['networks_values']       = template_nets
        extra_template['networks_values'] = []
    end

    return template if extra_roles.empty?

    roles.each_with_index do |role, index|
        extra_role = extra_roles.find {|item| item['name'] == role['name'] }
        next unless extra_role

        roles[index] = role.deep_merge(extra_role)
    end

    extra_template.delete('roles')

    template
end
instantiate(merge_template) click to toggle source
# File lib/opennebula/flow/service_template.rb, line 571
def instantiate(merge_template)
    rc = nil

    if merge_template.nil?
        instantiate_template = JSON.parse(@body.to_json)
    else
        @body = handle_nested_values(@body, merge_template)

        instantiate_template = JSON.parse(@body.to_json)
                                   .deep_merge(merge_template)
    end

    begin
        ServiceTemplate.validate(instantiate_template)

        xml     = OpenNebula::Service.build_xml
        service = OpenNebula::Service.new(xml, @client)

        rc = service.allocate(instantiate_template.to_json)
    rescue Validator::ParseException, JSON::ParserError => e
        return e
    end

    return rc if OpenNebula.is_error?(rc)

    service.info
    service
end
template() click to toggle source

Retrieves the template

@return [String] json template

# File lib/opennebula/flow/service_template.rb, line 368
def template
    @body.to_json
end
update(template_json, append = false) click to toggle source

Replaces the template contents

@param template_json [String] New template contents @param append [true, false] True to append new attributes instead of

replace the whole template

@return [nil, OpenNebula::Error] nil in case of success, Error

otherwise
Calls superclass method OpenNebula::DocumentJSON#update
# File lib/opennebula/flow/service_template.rb, line 380
def update(template_json, append = false)
    rc = info

    return rc if OpenNebula.is_error?(rc)

    template = JSON.parse(template_json)

    if append
        IMMUTABLE_ATTRS.each do |attr|
            unless template[attr].nil?
                return [false, "service_template/#{attr}"]
            end
        end
    else
        IMMUTABLE_ATTRS.each do |attr|
            # Allows updating the template without
            # specifying the immutable attributes
            if template[attr].nil?
                template[attr] = @body[attr]
            end

            next if template[attr] == @body[attr]

            return [false, "service_template/#{attr}"]
        end
    end

    template = @body.merge(template) if append

    ServiceTemplate.validate(template)

    super(template.to_json)
end
update_raw(template_raw, append = false) click to toggle source

Replaces the raw template contents

@param template [String] New template contents, in the form KEY = VAL @param append [true, false] True to append new attributes instead of

replace the whole template

@return [nil, OpenNebula::Error] nil in case of success, Error

otherwise
Calls superclass method OpenNebula::DocumentJSON#update_raw
# File lib/opennebula/flow/service_template.rb, line 526
def update_raw(template_raw, append = false)
    super(template_raw, append)
end
vm_template_ids() click to toggle source

Retreives all associated VM templates IDs

@return [Array] VM templates IDs

# File lib/opennebula/flow/service_template.rb, line 629
def vm_template_ids
    rc = info

    return rc if OpenNebula.is_error?(rc)

    ret = []

    @body['roles'].each do |role|
        t_id = Integer(role['template_id'])
        ret << t_id unless ret.include?(t_id)
    end

    ret
end