Ensure vm is present on the computenode

    name : string
        hostname of vm
    vmconfig : dict
        options to set for the vm
    config : dict
        fine grain control over vm_present

    .. note::

        The following configuration properties can be toggled in the config parameter.
          - kvm_reboot (true)                - reboots of kvm zones if needed for a config update
          - auto_import (false)              - automatic importing of missing images
          - auto_lx_vars (true)              - copy kernel_version and docker:* variables from image
          - reprovision (false)              - reprovision on image_uuid changes
          - enforce_tags (true)              - false = add tags only, true =  add, update, and remove tags
          - enforce_routes (true)            - false = add tags only, true =  add, update, and remove routes
          - enforce_internal_metadata (true) - false = add metadata only, true =  add, update, and remove metadata
          - enforce_customer_metadata (true) - false = add metadata only, true =  add, update, and remove metadata

    .. note::

        State ID is used as hostname. Hostnames must be unique.

    .. note::

        If hostname is provided in vmconfig this will take president over the State ID.
        This allows multiple states to be applied to the same vm.

    .. note::

        The following instances should have a unique ID.
          - nic : mac
          - filesystem: target
          - disk : path or diskN for zvols

        e.g. disk0 will be the first disk added, disk1 the 2nd,...

    .. versionchanged:: 2019.2.0

        Added support for docker image uuids, added auto_lx_vars configuration, documented some missing configuration options.

def vm_present(name, vmconfig, config=None):
    '''
    Ensure vm is present on the computenode

    name : string
        hostname of vm
    vmconfig : dict
        options to set for the vm
    config : dict
        fine grain control over vm_present

    .. note::

        The following configuration properties can be toggled in the config parameter.
          - kvm_reboot (true)                - reboots of kvm zones if needed for a config update
          - auto_import (false)              - automatic importing of missing images
          - auto_lx_vars (true)              - copy kernel_version and docker:* variables from image
          - reprovision (false)              - reprovision on image_uuid changes
          - enforce_tags (true)              - false = add tags only, true =  add, update, and remove tags
          - enforce_routes (true)            - false = add tags only, true =  add, update, and remove routes
          - enforce_internal_metadata (true) - false = add metadata only, true =  add, update, and remove metadata
          - enforce_customer_metadata (true) - false = add metadata only, true =  add, update, and remove metadata

    .. note::

        State ID is used as hostname. Hostnames must be unique.

    .. note::

        If hostname is provided in vmconfig this will take president over the State ID.
        This allows multiple states to be applied to the same vm.

    .. note::

        The following instances should have a unique ID.
          - nic : mac
          - filesystem: target
          - disk : path or diskN for zvols

        e.g. disk0 will be the first disk added, disk1 the 2nd,...

    .. versionchanged:: 2019.2.0

        Added support for docker image uuids, added auto_lx_vars configuration, documented some missing configuration options.

    '''
    name = name.lower()
    ret = {'name': name,
           'changes': {},
           'result': None,
           'comment': ''}

    # config defaults
    state_config = config if config else {}
    config = {
        'kvm_reboot': True,
        'auto_import': False,
        'auto_lx_vars': True,
        'reprovision': False,
        'enforce_tags': True,
        'enforce_routes': True,
        'enforce_internal_metadata': True,
        'enforce_customer_metadata': True,
    }
    config.update(state_config)
    log.debug('smartos.vm_present::%s::config - %s', name, config)

    # map special vmconfig parameters
    #  collections have set/remove handlers
    #  instances have add/update/remove handlers and a unique id
    vmconfig_type = {
        'collection': [
            'tags',
            'customer_metadata',
            'internal_metadata',
            'routes'
        ],
        'instance': {
            'nics': 'mac',
            'disks': 'path',
            'filesystems': 'target'
        },
        'create_only': [
            'filesystems'
        ]
    }
    vmconfig_docker_keep = [
        'docker:id',
        'docker:restartcount',
    ]
    vmconfig_docker_array = [
        'docker:env',
        'docker:cmd',
        'docker:entrypoint',
    ]

    # parse vmconfig
    vmconfig = _parse_vmconfig(vmconfig, vmconfig_type['instance'])
    log.debug('smartos.vm_present::%s::vmconfig - %s', name, vmconfig)

    # set hostname if needed
    if 'hostname' not in vmconfig:
        vmconfig['hostname'] = name

    # prepare image_uuid
    if 'image_uuid' in vmconfig:
        # NOTE: lookup uuid from docker uuid (normal uuid's are passed throuhg unmodified)
        #       we must do this again if we end up importing a missing image later!
        docker_uuid = __salt__['imgadm.docker_to_uuid'](vmconfig['image_uuid'])
        vmconfig['image_uuid'] = docker_uuid if docker_uuid else vmconfig['image_uuid']

        # NOTE: import image (if missing and allowed)
        if vmconfig['image_uuid'] not in __salt__['imgadm.list']():
            if config['auto_import']:
                if not __opts__['test']:
                    res = __salt__['imgadm.import'](vmconfig['image_uuid'])
                    vmconfig['image_uuid'] = __salt__['imgadm.docker_to_uuid'](vmconfig['image_uuid'])
                    if vmconfig['image_uuid'] not in res:
                        ret['result'] = False
                        ret['comment'] = 'failed to import image {0}'.format(vmconfig['image_uuid'])
            else:
                ret['result'] = False
                ret['comment'] = 'image {0} not installed'.format(vmconfig['image_uuid'])

    # prepare disk.*.image_uuid
    for disk in vmconfig['disks'] if 'disks' in vmconfig else []:
        if 'image_uuid' in disk and disk['image_uuid'] not in __salt__['imgadm.list']():
            if config['auto_import']:
                if not __opts__['test']:
                    res = __salt__['imgadm.import'](disk['image_uuid'])
                    if disk['image_uuid'] not in res:
                        ret['result'] = False
                        ret['comment'] = 'failed to import image {0}'.format(disk['image_uuid'])
            else:
                ret['result'] = False
                ret['comment'] = 'image {0} not installed'.format(disk['image_uuid'])

    # docker json-array handling
    if 'internal_metadata' in vmconfig:
        for var in vmconfig_docker_array:
            if var not in vmconfig['internal_metadata']:
                continue
            if isinstance(vmconfig['internal_metadata'][var], list):
                vmconfig['internal_metadata'][var] = json.dumps(
                    vmconfig['internal_metadata'][var]
                )

    # copy lx variables
    if vmconfig['brand'] == 'lx' and config['auto_lx_vars']:
        # NOTE: we can only copy the lx vars after the image has bene imported
        vmconfig = _copy_lx_vars(vmconfig)

    # quick abort if things look wrong
    # NOTE: use explicit check for false, otherwise None also matches!
    if ret['result'] is False:
        return ret

    # check if vm exists
    if vmconfig['hostname'] in __salt__['vmadm.list'](order='hostname'):
        # update vm
        ret['result'] = True

        # expand vmconfig
        vmconfig = {
            'state': vmconfig,
            'current': __salt__['vmadm.get'](vmconfig['hostname'], key='hostname'),
            'changed': {},
            'reprovision_uuid': None
        }

        # prepare reprovision
        if 'image_uuid' in vmconfig['state']:
            vmconfig['reprovision_uuid'] = vmconfig['state']['image_uuid']
            vmconfig['state']['image_uuid'] = vmconfig['current']['image_uuid']

        # disks need some special care
        if 'disks' in vmconfig['state']:
            new_disks = []
            for disk in vmconfig['state']['disks']:
                path = False
                if 'disks' in vmconfig['current']:
                    for cdisk in vmconfig['current']['disks']:
                        if cdisk['path'].endswith(disk['path']):
                            path = cdisk['path']
                            break
                if not path:
                    del disk['path']
                else:
                    disk['path'] = path
                new_disks.append(disk)
            vmconfig['state']['disks'] = new_disks

        # process properties
        for prop in vmconfig['state']:
            # skip special vmconfig_types
            if prop in vmconfig_type['instance'] or \
                prop in vmconfig_type['collection'] or \
                prop in vmconfig_type['create_only']:
                continue

            # skip unchanged properties
            if prop in vmconfig['current']:
                if isinstance(vmconfig['current'][prop], (list)) or isinstance(vmconfig['current'][prop], (dict)):
                    if vmconfig['current'][prop] == vmconfig['state'][prop]:
                        continue
                else:
                    if "{0}".format(vmconfig['current'][prop]) == "{0}".format(vmconfig['state'][prop]):
                        continue

            # add property to changeset
            vmconfig['changed'][prop] = vmconfig['state'][prop]

        # process collections
        for collection in vmconfig_type['collection']:
            # skip create only collections
            if collection in vmconfig_type['create_only']:
                continue

            # enforcement
            enforce = config['enforce_{0}'.format(collection)]
            log.debug('smartos.vm_present::enforce_%s = %s', collection, enforce)

            # dockerinit handling
            if collection == 'internal_metadata' and vmconfig['state'].get('docker', False):
                if 'internal_metadata' not in vmconfig['state']:
                    vmconfig['state']['internal_metadata'] = {}

                # preserve some docker specific metadata (added and needed by dockerinit)
                for var in vmconfig_docker_keep:
                    val = vmconfig['current'].get(collection, {}).get(var, None)
                    if val is not None:
                        vmconfig['state']['internal_metadata'][var] = val

            # process add and update for collection
            if collection in vmconfig['state'] and vmconfig['state'][collection] is not None:
                for prop in vmconfig['state'][collection]:
                    # skip unchanged properties
                    if prop in vmconfig['current'][collection] and \
                        vmconfig['current'][collection][prop] == vmconfig['state'][collection][prop]:
                        continue

                    # skip update if not enforcing
                    if not enforce and prop in vmconfig['current'][collection]:
                        continue

                    # create set_ dict
                    if 'set_{0}'.format(collection) not in vmconfig['changed']:
                        vmconfig['changed']['set_{0}'.format(collection)] = {}

                    # add property to changeset
                    vmconfig['changed']['set_{0}'.format(collection)][prop] = vmconfig['state'][collection][prop]

            # process remove for collection
            if enforce and collection in vmconfig['current'] and vmconfig['current'][collection] is not None:
                for prop in vmconfig['current'][collection]:
                    # skip if exists in state
                    if collection in vmconfig['state'] and vmconfig['state'][collection] is not None:
                        if prop in vmconfig['state'][collection]:
                            continue

                    # create remove_ array
                    if 'remove_{0}'.format(collection) not in vmconfig['changed']:
                        vmconfig['changed']['remove_{0}'.format(collection)] = []

                    # remove property
                    vmconfig['changed']['remove_{0}'.format(collection)].append(prop)

        # process instances
        for instance in vmconfig_type['instance']:
            # skip create only instances
            if instance in vmconfig_type['create_only']:
                continue

            # add or update instances
            if instance in vmconfig['state'] and vmconfig['state'][instance] is not None:
                for state_cfg in vmconfig['state'][instance]:
                    add_instance = True

                    # find instance with matching ids
                    for current_cfg in vmconfig['current'][instance]:
                        if vmconfig_type['instance'][instance] not in state_cfg:
                            continue

                        if state_cfg[vmconfig_type['instance'][instance]] == current_cfg[vmconfig_type['instance'][instance]]:
                            # ids have matched, disable add instance
                            add_instance = False

                            changed = _get_instance_changes(current_cfg, state_cfg)
                            update_cfg = {}

                            # handle changes
                            for prop in changed:
                                update_cfg[prop] = state_cfg[prop]

                            # handle new properties
                            for prop in state_cfg:
                                # skip empty props like ips, options,..
                                if isinstance(state_cfg[prop], (list)) and not state_cfg[prop]:
                                    continue

                                if prop not in current_cfg:
                                    update_cfg[prop] = state_cfg[prop]

                            # update instance
                            if update_cfg:
                                # create update_ array
                                if 'update_{0}'.format(instance) not in vmconfig['changed']:
                                    vmconfig['changed']['update_{0}'.format(instance)] = []

                                update_cfg[vmconfig_type['instance'][instance]] = state_cfg[vmconfig_type['instance'][instance]]
                                vmconfig['changed']['update_{0}'.format(instance)].append(update_cfg)

                    if add_instance:
                        # create add_ array
                        if 'add_{0}'.format(instance) not in vmconfig['changed']:
                            vmconfig['changed']['add_{0}'.format(instance)] = []

                        # add instance
                        vmconfig['changed']['add_{0}'.format(instance)].append(state_cfg)

            # remove instances
            if instance in vmconfig['current'] and vmconfig['current'][instance] is not None:
                for current_cfg in vmconfig['current'][instance]:
                    remove_instance = True

                    # find instance with matching ids
                    if instance in vmconfig['state'] and vmconfig['state'][instance] is not None:
                        for state_cfg in vmconfig['state'][instance]:
                            if vmconfig_type['instance'][instance] not in state_cfg:
                                continue

                            if state_cfg[vmconfig_type['instance'][instance]] == current_cfg[vmconfig_type['instance'][instance]]:
                                # keep instance if matched
                                remove_instance = False

                    if remove_instance:
                        # create remove_ array
                        if 'remove_{0}'.format(instance) not in vmconfig['changed']:
                            vmconfig['changed']['remove_{0}'.format(instance)] = []

                        # remove instance
                        vmconfig['changed']['remove_{0}'.format(instance)].append(
                            current_cfg[vmconfig_type['instance'][instance]]
                        )

        # update vm if we have pending changes
        kvm_needs_start = False
        if not __opts__['test'] and vmconfig['changed']:
            # stop kvm if disk updates and kvm_reboot
            if vmconfig['current']['brand'] == 'kvm' and config['kvm_reboot']:
                if 'add_disks' in vmconfig['changed'] or \
                    'update_disks' in vmconfig['changed'] or \
                    'remove_disks' in vmconfig['changed']:
                    if vmconfig['state']['hostname'] in __salt__['vmadm.list'](order='hostname', search='state=running'):
                        kvm_needs_start = True
                        __salt__['vmadm.stop'](vm=vmconfig['state']['hostname'], key='hostname')

            # do update
            rret = __salt__['vmadm.update'](vm=vmconfig['state']['hostname'], key='hostname', **vmconfig['changed'])
            if not isinstance(rret, (bool)) and 'Error' in rret:
                ret['result'] = False
                ret['comment'] = "{0}".format(rret['Error'])
            else:
                ret['result'] = True
                ret['changes'][vmconfig['state']['hostname']] = vmconfig['changed']

        if ret['result']:
            if __opts__['test']:
                ret['changes'][vmconfig['state']['hostname']] = vmconfig['changed']

            if vmconfig['state']['hostname'] in ret['changes'] and ret['changes'][vmconfig['state']['hostname']]:
                ret['comment'] = 'vm {0} updated'.format(vmconfig['state']['hostname'])
                if config['kvm_reboot'] and vmconfig['current']['brand'] == 'kvm' and not __opts__['test']:
                    if vmconfig['state']['hostname'] in __salt__['vmadm.list'](order='hostname', search='state=running'):
                        __salt__['vmadm.reboot'](vm=vmconfig['state']['hostname'], key='hostname')
                    if kvm_needs_start:
                        __salt__['vmadm.start'](vm=vmconfig['state']['hostname'], key='hostname')
            else:
                ret['changes'] = {}
                ret['comment'] = 'vm {0} is up to date'.format(vmconfig['state']['hostname'])

            # reprovision (if required and allowed)
            if 'image_uuid' in vmconfig['current'] and vmconfig['reprovision_uuid'] != vmconfig['current']['image_uuid']:
                if config['reprovision']:
                    rret = __salt__['vmadm.reprovision'](
                        vm=vmconfig['state']['hostname'],
                        key='hostname',
                        image=vmconfig['reprovision_uuid']
                    )
                    if not isinstance(rret, (bool)) and 'Error' in rret:
                        ret['result'] = False
                        ret['comment'] = 'vm {0} updated, reprovision failed'.format(
                            vmconfig['state']['hostname']
                        )
                    else:
                        ret['comment'] = 'vm {0} updated and reprovisioned'.format(vmconfig['state']['hostname'])
                        if vmconfig['state']['hostname'] not in ret['changes']:
                            ret['changes'][vmconfig['state']['hostname']] = {}
                        ret['changes'][vmconfig['state']['hostname']]['image_uuid'] = vmconfig['reprovision_uuid']
                else:
                    log.warning('smartos.vm_present::%s::reprovision - '
                                'image_uuid in state does not match current, '
                                'reprovision not allowed',
                                name)
        else:
            ret['comment'] = 'vm {0} failed to be updated'.format(vmconfig['state']['hostname'])
            if not isinstance(rret, (bool)) and 'Error' in rret:
                ret['comment'] = "{0}".format(rret['Error'])
    else:
        # check required image installed
        ret['result'] = True

        # disks need some special care
        if 'disks' in vmconfig:
            new_disks = []
            for disk in vmconfig['disks']:
                if 'path' in disk:
                    del disk['path']
                new_disks.append(disk)
            vmconfig['disks'] = new_disks

        # create vm
        if ret['result']:
            uuid = __salt__['vmadm.create'](**vmconfig) if not __opts__['test'] else True
            if not isinstance(uuid, (bool)) and 'Error' in uuid:
                ret['result'] = False
                ret['comment'] = "{0}".format(uuid['Error'])
            else:
                ret['result'] = True
                ret['changes'][vmconfig['hostname']] = vmconfig
                ret['comment'] = 'vm {0} created'.format(vmconfig['hostname'])

    return ret