#  Licensed under the Apache License, Version 2.0 (the "License"); you may
#  not use this file except in compliance with the License. You may obtain
#  a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#  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.

Namespaces:
  =: io.murano.resources
  std: io.murano
  sys: io.murano.system


Name: Instance
Extends:
  - std:CloudResource
  - MetadataAware

Properties:
  name:
    Contract: $.string().notNull()
  flavor:
    Contract: $.string().notNull()
  image:
    Contract: $.string()
    Default: null
  keyname:
    Contract: $.string()
    Default: null
  openstackId:
    Contract: $.string()
    Usage: Out
  availabilityZone:
    Contract: $.string().notNull()
    Default: nova
  agent:
    Contract: $.class(sys:Agent)
    Usage: Runtime
  ipAddresses:
    Contract: [$.string()]
    Usage: Out
  networks:
    Contract:
      useEnvironmentNetwork: $.bool().notNull()
      useFlatNetwork: $.bool().notNull()
      customNetworks: [$.class(Network).notNull()]
      primaryNetwork: $.class(Network).notOwned()
    Default:
      useEnvironmentNetwork: true
      useFlatNetwork: false
      customNetworks: []
      primaryNetwork: null
  assignFloatingIp:
    Contract: $.bool().notNull()
    Default: false
  floatingIpAddress:
    Contract: $.string()
    Usage: Out
  securityGroupName:
    Contract: $.string()
    Default: null
  securityGroups:
    Contract: [$.string()]
    Default: []
  sharedIps:
    Contract:
      - $.class(std:SharedIp)
    Usage: InOut  # as it is set in setSharedIps
  volumes:
    Contract:
      $.string().notNull(): $.class(Volume).notNull()
  blockDevices:
    Contract:
      - volume: $.class(Volume).notNull()
        deviceName: $.string()
        deviceType: $.string().check($ in list(disk, cdrom, null))
        bootIndex: $.int()
    Default: []
  joinedNetworks:
    Contract:
      - network: $.class(Network).notNull()
        ipList: [$.string().notNull()]
    Usage: Out
    #policies: anti-affinity, affinity
  instanceAffinityGroup:
    Contract: $.class(InstanceAffinityGroup)

Methods:
  .init:
    Body:
      - $._environment: $.find(std:Environment).require()
      - $.agent: new(sys:Agent, host => $)
      - $.resources: new(sys:Resources)
      - $.instanceTemplate: {}
      - $._floatingIpOutputName: null

      - $.volumes.values().concat($this.blockDevices.volume).select(
          sys:GC.subscribeDestruction($, $this, _onReferencedResourceDelete))

      - If: $.instanceAffinityGroup != null
        Then: sys:GC.subscribeDestruction(
          $this.instanceAffinityGroup, $this, _onReferencedResourceDelete)


  # Called after the Instance template pieces are in place. It
  # is at this stage alterations to the template should be made
  prepareStackTemplate:
    Arguments:
      instanceTemplate:
        Contract: {}
    Body:
      - Return: $instanceTemplate

  setSharedIps:
    Arguments:
      ips:
        Contract:
          - $.class(std:SharedIp)
    Body:
      $.sharedIps: $ips

  beginDeploy:
    Body:
      - $.validateBootSource()
      - $region: $.getRegion()
      - $securityGroupName: coalesce(
            $.securityGroupName,
            $region.securityGroupManager.defaultGroupName
          )
      - $.createDefaultInstanceSecurityGroupRules($securityGroupName)
      - $.detectPrimaryNetwork()
      - $.ensureSharedIpsDeployed()
      - $.ensureNetworksDeployed()
      - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null
        Then:
          $.joinNet($region.defaultNetworks.environment, $securityGroupName, $this.securityGroups)
      - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null
        Then:
          $.joinNet($region.defaultNetworks.flat, $securityGroupName, $this.securityGroups)
      - $.networks.customNetworks.select($this.joinNet($, $securityGroupName, $this.securityGroups))

      - $preparedUserData: $.prepareUserData()
      - $properties:
          name: $.name
          flavor: $.flavor
          availability_zone: $.availabilityZone
          user_data: $preparedUserData.data
          user_data_format: $preparedUserData.format
          key_name: $.keyname
          metadata: $this.getMetadata($region)

      - If: len($.blockDevices) > 0
        Then:
          - $bdmDefinition: $.blockDevices.select($this.buidBlockDeviceDefinition($))
          - $properties.block_device_mapping_v2: $bdmDefinition
        Else:
          $properties.image: $.image
      - If: $.instanceAffinityGroup != null
        Then:
          - $.instanceAffinityGroup.deploy()
          - $properties.scheduler_hints:
              group: $.instanceAffinityGroup.getRef()
      - $template:
          resources:
            $.name:
              type: $this.getResourceType()
              properties: $properties

          outputs:
            format('{0}-id', $.name):
              description: format('ID of {0} instance', $.name)
              value:
                get_resource: $.name
      - $.instanceTemplate: $.instanceTemplate.mergeWith($template)
      - $.volumes.items().select(
          $.unpack(path, volume) -> $this.attachVolume($path, $volume))

      # Any additional template preparation
      - $.instanceTemplate: $.prepareStackTemplate($.instanceTemplate)
      - $region.stack.updateTemplate($.instanceTemplate)

  endDeploy:
    Body:
      - $region: $.getRegion()
      - $region.stack.push()
      - $outputs: $region.stack.output()
      - For: blockDevice
        In: $.blockDevices
        Do:
          - $blockDevice.volume.setAttachments($outputs.get(format('{0}-attachments', $blockDevice.volume.getResourceName())))
      - $.openstackId: $outputs.get(format('{0}-id', $this.name))
      - If: $._floatingIpOutputName != null
        Then:
          - $.floatingIpAddress: $outputs.get($._floatingIpOutputName)
          - If: $.floatingIpAddress != null
            Then: $.setAttr(fipAssigned, true)
      - $._environment.instanceNotifier.trackCloudInstance($this)
      - $.joinedNetworks: $this.joinedNetworks.distinct().select({
          network => $.network,
          ipList => $.network.getInstanceIpList($this)})
      - $.ipAddresses: $this.joinedNetworks.selectMany($.ipList).where($ != $this.floatingIpAddress).distinct()
      - If: $.floatingIpAddress != null
        Then:
          - $.ipAddresses: $.ipAddresses.append($.floatingIpAddress)

  deploy:
    Body:
      - $this.beginDeploy()
      - $this.endDeploy()

  detectPrimaryNetwork:
    Body:
      - $region: $.getRegion()
      - $._primaryNetwork: null
      - If: $.networks.primaryNetwork != null
        Then:
          - $._primaryNetwork: $.networks.primaryNetwork
        Else:
          - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null
            Then:
              - $._primaryNetwork: $region.defaultNetworks.environment
            Else:
              - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null
                Then:
                  - $._primaryNetwork: $region.defaultNetworks.flat
                Else:
                  - If: $.networks.customNetworks!= null
                    Then:
                      - $._primaryNetwork: $.networks.customNetworks.first(null)

  ensureNetworksDeployed:
    Body:
      - $region: $.getRegion()
      - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null
        Then:
          - $region.defaultNetworks.environment.deploy()
      - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null
        Then:
          - $region.defaultNetworks.flat.deploy()
      - $.networks.customNetworks.pselect($.deploy())

  ensureSharedIpsDeployed:
    Body:
      - $.sharedIps.pselect($.deploy())

  joinNet:
    Arguments:
      - net:
          Contract: $.class(Network).notNull()
      - securityGroupName:
          Contract: $.string()
      - securityGroups:
          Contract: [$.string()]
    Body:
      - $primary: $net = $._primaryNetwork
      - $assignFip: $primary and $.assignFloatingIp and not $.getAttr(fipAssigned, false)

      - $sharedIps: []
      - For: sharedIp
        In: $.sharedIps
        Do:
          - If: $sharedIp.network = $net
            Then:
              - $sharedIps: $sharedIps.append($sharedIp)

      - $joinResult: $net.joinInstance(
            instance => $this,
            securityGroupName => $securityGroupName,
            securityGroups => $securityGroups,
            assignFloatingIp => $assignFip,
            sharedIps => $sharedIps
          )
      - $.setAttr(instanceResources, $.getAttr(instanceResources, []).concat($joinResult.get(instanceResources, [])))
      - $.setAttr(instanceOutputs, $.getAttr(instanceOutputs, []).concat($joinResult.get(instanceOutputs, [])))

      - If: $joinResult.template != null
        Then:
          - $.instanceTemplate: $.instanceTemplate.mergeWith($joinResult.template)

      - If: $joinResult.get(portRef) != null
        Then:
          - $template:
              resources:
                $.name:
                  properties:
                    networks:
                      - port:
                          $joinResult.portRef
          - $.instanceTemplate: $.instanceTemplate.mergeWith($template)
      - If: $joinResult.get(secGroupName) != null
        Then:
          - $template:
              resources:
                $.name:
                  properties:
                    security_groups:
                      - $joinResult.secGroupName
          - $.instanceTemplate: $.instanceTemplate.mergeWith($template)

      - $._floatingIpOutputName: coalesce($._floatingIpOutputName, $joinResult.instanceFipOutput)
      - $this.joinedNetworks: $this.joinedNetworks.append({network => $net})

  attachVolume:
    Arguments:
      - path:
          Contract: $.string().notNull()
      - volume:
          Contract: $.class(Volume).notNull()
    Body:
      - $attachment: $volume.attachTo($this, $path)
      - $.instanceTemplate: $.instanceTemplate.mergeWith($attachment.template)
      - $.setAttr(instanceResources, $.getAttr(instanceResources, []).concat($attachment.get(instanceResources, [])))
      - $.setAttr(instanceOutputs, $.getAttr(instanceOutputs, []).concat($attachment.get(instanceOutputs, [])))

  buidBlockDeviceDefinition:
    Arguments:
      blockDevice:
        Contract:
          volume: $.class(Volume).notNull()
          deviceName: $.string()
          deviceType: $.string().check($ in list(disk, cdrom, null))
          bootIndex: $.int()

    Body:
      - $blockDevice.volume.deploy()
      - Return:
          device_name: $blockDevice.deviceName
          volume_id: $blockDevice.volume.getRef()
          device_type: $blockDevice.deviceType
          boot_index: $blockDevice.bootIndex

  beginReleaseResources:
    Body:
      - $region: $.getRegion()
      - $template: $region.stack.current()
      - If: $template.get(resources) and $template.get(outputs)
        Then:
          - $resourcesToDelete: list($.name).concat($.getAttr(instanceResources, []))
          - $lenBefore: len($template.resources)
          - $template.resources: $template.resources.deleteAll($resourcesToDelete)
          - If: $lenBefore > len($template.resources)
            Then:
              - $._environment.instanceNotifier.untrackCloudInstance($this)

          - $outputsToDelete: list(
              '{0}-id'.format($.name)).concat($.getAttr(instanceOutputs, []))
          - $template.outputs: $template.outputs.deleteAll($outputsToDelete)

          - $region.stack.setTemplate($template)

  _onReferencedResourceDelete:
    Arguments:
      - resource:
          Contract: $.class(std:CloudResource).notNull()
    Body:
      - If: sys:GC.isDoomed($this)
        Then:
          - $.beginReleaseResources()

  endReleaseResources:
    Body:
      - $region: $.getRegion()
      - $template: $region.stack.current()
      - If: $template.get(resources) and $template.get(outputs) and not sys:GC.isDoomed($region)
        Then:
          - $region.stack.push(async => true)
      - $.setAttr(instanceResources, [])
      - $.setAttr(instanceOutputs, [])
      - $.setAttr(fipAssigned, false)
      - $.openstackId: null
      - $.ipAddresses: []
      - $.floatingIpAddress: null

  releaseResources:
    Body:
      - $this.beginReleaseResources()
      - $this.endReleaseResources()

  validateBootSource:
    Body:
      - If: $.image = null and len($.blockDevices) = 0
        Then:
          - $msg: format('Neither image nor bootable volumes is specified for instance {0}', $.name)
          - Throw: ResourceNotFound
            Message: $msg
      - If: $.image != null and len($.blockDevices) > 0
        Then:
          - $msg: 'Both image and list of bootable volumes are specified'
          - Throw: ResourceConflict
            Message: $msg

  destroy:
    Body:
      - $.releaseResources()


  createDefaultInstanceSecurityGroupRules:
    Arguments:
      - groupName:
          Contract: $.string().notNull()

  prepareUserData:
    Body:
      Return:
        data: null
        # Valid values are HEAT_CFNTOOLS, RAW and SOFTWARE_CONFIG
        format: HEAT_CFNTOOLS

  isDeployed:
    Body:
      - $region: $.getRegion()
      - $template: $region.stack.current()
      - Return: $template.get(resources, {}).containsKey($.name)

  getRef:
    Body:
      Return:
        get_resource: $.name

  getResourceType:
    Body:
      - Return: 'OS::Nova::Server'
