Namespaces:
  =: io.murano.applications
  std: io.murano
  res: io.murano.resources
  m: io.murano.metadata.engine

--- # ------------------------------------------------------------------ # ---

Name: Installable

Properties:
  allowedInstallFailures:
    Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum'])
    Default: 'none'

  beforeInstallEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: beforeInstall

  installServerEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: installServer

  completeInstallationEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: completeInstallation

Methods:
  install:
    Arguments:
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $serversToInstall: $serverGroup.getServers().pselect(
          switch($this.checkServerIsInstalled($) => null, true => $)).where($ != null)
      - If: any($serversToInstall)
        Then:
          - $.beforeInstall($serversToInstall, $serverGroup)
          - $failures: $serversToInstall.pselect($this.installServer($, $serverGroup)).where($ != null)
          - $.completeInstallation($serversToInstall, $serverGroup, $failures)

  checkServerIsInstalled:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance)
    Body:
      - Return: $server.getAttr(installed, false)

  beforeInstall:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $this.report(format('Installing {0}', name($this)))
      - $this.beforeInstallEvent.notify($this, $servers, $serverGroup)
      - $this.onBeforeInstall($servers, $serverGroup)

  onBeforeInstall:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()

  installServer:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      Try:
        - $this.report(format('Began installing {0} on {1}', name($this),
                              $server.name))
        - $this.installServerEvent.notify($this, $server, $serverGroup)
        - $this.onInstallServer($server, $serverGroup)

      Catch:
        - As: e
          Do:
            - $this.report(format('Unable to install {0} on {1} due to {2}',
                                  name($this), $server.name, $e.message))
            - Return: $server
      Else:
        - $this.report(format('{0} is installed on {1}',
                              name($this),
                              $server.name))
        - $server.setAttr(installed, true)
        - Return: null

  onInstallServer:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()

  completeInstallation:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
      - failedServers:
          Contract:
            - $.class(res:Instance).notNull()
    Body:
      - $success: :SoftwareComponent.detectSuccess($this.allowedInstallFailures, $serverGroup, $failedServers)
      - If: $success
        Then:
          - $this.completeInstallationEvent.notify($this, $servers, $serverGroup, $failedServers)
          - $this.onCompleteInstallation($servers, $serverGroup, $failedServers)

          - $this.report(format('Finished installing {0} ({1} errors encountered)',
                                name($this), len($failedServers) or 'no'))
        Else:
          - Throw: TooManyInstallationErrors
            Message: format('Too many errors ({0}) encountered while installing {1}',
                            len($failedServers), name($this))

  onCompleteInstallation:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
      - failedServers:
          Contract:
            - $.class(res:Instance).notNull()

  report:
    Arguments:
      - message:
          Contract: $.string().notNull()
    Body:
      - $env: $this.find(std:Environment)
      - If: $env
        Then:
          - $env.reporter.report($this, $message)

--- # ------------------------------------------------------------------ # ---

Name: Configurable

Properties:
  allowedConfigurationFailures:
    Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum'])
    Default: 'none'

  preConfigureEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: preConfigure

  configureServerEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: configureServer

  completeConfigurationEvent:
    Contract: $.class(Event).notNull()
    Usage: Runtime
    Default:
      name: completeConfiguration

Methods:
  .init:
    Body:
      - $this._randomName: randomName()

  configure:
    Arguments:
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - If: $this.checkClusterIsConfigured($serverGroup)
        Then:
          - Return:
      - $serversToConfigure: $serverGroup.getServers().pselect(
          switch($this.checkServerIsConfigured($, $serverGroup) => null, true => $)).where($ != null)
      - $this.preConfigure($serversToConfigure, $serverGroup)
      - $failures: $serversToConfigure.pselect($this.configureServer($, $serverGroup)).where($ != null)
      - $.completeConfiguration($serversToConfigure, $serverGroup, $failures)

  checkClusterIsConfigured:
    Arguments:
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $key: list($this.getConfigurationKey()) + $serverGroup.getKey()
      - $state: $serverGroup.getAttr(configuration)
      - Return: $key = $state

  checkServerIsConfigured:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $key: $this.getConfigurationKey()
      - $state: $server.getAttr(configuration, null)
      - Return: $key = $state

  getConfigurationKey:
    Body:
      # should be redefined in subclasses to contain semantical signature
      # of the object's configuration
      - Return: $this._randomName

  preConfigure:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $this.report(format('Applying configuration of {0}', name($this)))
      - $this.configureSecurity($servers, $serverGroup)
      - $this.preConfigureEvent.notify($this, $servers, $serverGroup)
      - $this.onPreConfigure($servers, $serverGroup)

  configureSecurity:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()

  onPreConfigure:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()

  configureServer:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - Try:
          - $this.report(format('Began configuring {0} on {1}',
                         name($this), $server.name))
          - $this.configureServerEvent.notify($this, $server, $serverGroup)
          - $this.onConfigureServer($server, $serverGroup)

        Catch:
          - As: e
            Do:
              - $this.report(format('Unable to configure {0} on {1} due to {2}',
                                    name($this), $server.name, $e.message))
              - Return: $server
        Else:
          - $key: $this.getConfigurationKey()
          - $server.setAttr(configuration, $key)
          - $this.report(format('{0} is configured at {1}', name($this), $server.name))
          - Return: null

  onConfigureServer:
    Meta:
      - m:Synchronize:
          onArgs: server
    Arguments:
      - server:
          Contract: $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()

  completeConfiguration:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
      - failedServers:
          Contract:
            - $.class(res:Instance).notNull()
    Body:
      - $success: :SoftwareComponent.detectSuccess($this.allowedConfigurationFailures, $serverGroup, $failedServers)
      - If: $success
        Then:
          - $this.completeConfigurationEvent.notify($this, $servers, $serverGroup, $failedServers)
          - $this.onCompleteConfiguration($servers, $serverGroup, $failedServers)
          - $key: list($this.getConfigurationKey()) + $serverGroup.getKey()
          - $serverGroup.setAttr(configuration, $key)
          - $this.report(format('Finished configuring {0} ({1} errors encountered)',
                         name($this), $numFailures or 'no'))
        Else:
          - Throw: TooManyConfigurationErrors
            Message: format('Too many errors ({0}) encountered while configuring {1}',
                            $numFailures, name($this))

  onCompleteConfiguration:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
      - failedServers:
          Contract:
            - $.class(res:Instance).notNull()

  report:
    Arguments:
      - message:
          Contract: $.string().notNull()
    Body:
      - $env: $this.find(std:Environment)
      - If: $env
        Then:
          - $env.reporter.report($this, $message)

--- # ------------------------------------------------------------------ # ---

Name: OpenStackSecurityConfigurable
Extends: Configurable

Methods:
  configureSecurity:
    Arguments:
      - servers:
          Contract:
            - $.class(res:Instance).notNull()
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $sr: $this.getSecurityRules()
      - If: $sr
        Then:
          - $regions: $servers.select($.getRegion()).distinct()
          - $regions.pselect($this._configureSecurityGroup($, $sr))

  _configureSecurityGroup:
    Usage: Static
    Arguments:
      - region:
          Contract: $.class(std:CloudRegion).notNull()
      - rules:
          Contract:
            - FromPort: $.int().notNull()
              ToPort: $.int().notNull()
              IpProtocol: $.string().notNull()
              External: $.bool().notNull()
              Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6'))
    Body:
      - $region.securityGroupManager.addGroupIngress($rules)
      - $region.stack.push()

  getSecurityRules:
    Body:
      - Return: {}

--- # ------------------------------------------------------------------ # ---
Name: SoftwareComponent
Extends:
  - Installable
  - Configurable

Methods:
  deployAt:
    Arguments:
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
    Body:
      - $serverGroup.deploy()
      - $this.install($serverGroup)
      - $this.configure($serverGroup)

  report:
    Arguments:
      - message:
          Contract: $.string().notNull()
    Body:
      - cast($this, Installable).report($message)

  detectSuccess:
    Usage: Static
    Arguments:
      - allowedFailures:
          Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum'])
      - serverGroup:
          Contract: $.class(ServerGroup).notNull()
      - failedServers:
          Contract:
            - $.class(res:Instance).notNull()
    Body:
      - $numFailures: len($failedServers)
      - Match:
          none:
            - Return: $numFailures = 0
          one:
            - Return: $numFailures <= 1
          two:
            - Return: $numFailures <= 2
          three:
            - Return: $numFailures <= 3
          any:
            - Return: true
          quorum:
            - $numServers: $serverGroup.getServers().count()
            - $maxFailures: $numServers - ($numServers/2 + 1)
            - Return: $numFailures <= $maxFailures
        Value: $allowedFailures
