SoftwareComponent implementation

Added two classed to implement two basic application workflows:
installation and software configuration, with appropriate error
handling, reporting, event-based notifications and so on. A class
called 'SoftwareComponent' inherits both these classes and is able to
run these two workflows sequentially to install and configure software
on given Instance Groups.

Change-Id: Ic0c1bbad1f0e3ae71b339c56db245075bcc420ac
Targets-blueprint: application-development-framework
This commit is contained in:
Alexander Tivelkov 2016-07-18 17:18:15 +03:00
parent c4c8be61e0
commit ef85e7ead9
4 changed files with 501 additions and 3 deletions

View File

@ -0,0 +1,367 @@
Namespaces:
=: io.murano.applications
std: io.murano
res: io.murano.resources
m: io.murano.metadata.engine
--- # ------------------------------------------------------------------ # ---
Name: BaseSoftwareComponent
Properties:
allowedFailures:
Contract: $.check($ in ['none', 'one', 'any', 'quorum'])
Default: 'none'
componentName:
Contract: $.string()
Usage: Runtime
Methods:
.init:
Body:
- $this._env: $.find(std:Environment).require()
- $this.componentName: name($this)
deployAt:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $servers.deploy()
report:
Arguments:
- message:
Contract: $.string().notNull()
Body:
- $this._env.reporter.report($this, $message)
_detect_success:
Arguments:
- numServers:
Contract: $.int()
- numFailures:
Contract: $.int()
Body:
- Switch:
$this.allowedFailures = none:
- Return: $numFailures = 0
$this.allowedFailures = one:
- Return: $numFailures <= 1
$this.allowedFailures = any:
- Return: true
$this.allowedFailures = quorum:
- $maxFailures: $numServers - ($numServers/2 + 1)
- Return: $numFailures <= $maxFailures
--- # ------------------------------------------------------------------ # ---
Name: Installable
Extends: BaseSoftwareComponent
Properties:
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:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $serversToInstall: $servers.items.where($this.checkServerNeedsInstallation($))
- If: any($serversToInstall)
Then:
- $.beforeInstall($servers)
- $results: $serversToInstall.pselect($this.installServer($))
- $.completeInstallation($servers, $results.where(not $).len())
checkServerNeedsInstallation:
Arguments:
- server:
Contract: $.class(res:Instance)
Body:
- Return: not $.getAttr(format('installed_at_{0}', id($server)), false)
beforeInstall:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $this.report(format('Installing {0}', $this.componentName))
- $this.onBeforeInstall($servers)
onBeforeInstall:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $this.beforeInstallEvent.notify($this, $servers)
installServer:
Meta:
- m:Synchronize:
onArgs: server
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
Body:
Try:
- $this.report(format('Began installing {0} on {1}', $this.componentName,
$server.name))
- $this.onInstallServer($server)
Catch:
- As: e
Do:
- $this.report(format('Unable to install {0} on {1} due to {2}',
$this.componentName, $server.name, $e.message))
- Return: False
Else:
- $this.report(format('{0} is installed on {1}',
$this.componentName,
$server.name))
- $.setAttr(format('installed_at_{0}', id($server)), true)
- Return: True
onInstallServer:
Meta:
- m:Synchronize:
onArgs: server
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
Body:
- $this.installServerEvent.notify($this, $server)
completeInstallation:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
- numFailures:
Contract: $.int()
Body:
- $success: $this._detect_success($servers.items.len(), $numFailures)
- If: $success
Then:
- $this.onCompleteInstallation($servers)
- $this.report(format('Finished installing {0} ({1} errors encountered)',
$this.componentName, $numFailures or 'no'))
Else:
- Throw: TooManyInstallationErrors
Message: format('Too many errors ({0}) encountered while installing {1}',
$numFailures, $this.componentName)
onCompleteInstallation:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $this.completeInstallationEvent.notify($this, $servers)
deployAt:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- super($this, $.deployAt($servers))
- $this.install($servers)
--- # ------------------------------------------------------------------ # ---
Name: Configurable
Extends: BaseSoftwareComponent
Properties:
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:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- If: not $this.checkClusterNeedsReconfiguration($servers)
Then:
- Return:
- $this.preConfigure($servers)
- $serversToConfigure: $servers.items.where($this.checkServerNeedsConfiguration($))
- $results: $serversToConfigure.pselect($this.configureServer($))
- $.completeConfiguration($servers, $results.where(not $).len())
checkClusterNeedsReconfiguration:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $key: $this.getKey()
- $state: $this.getAttr(format('configuration_of_cluster_{0}', id($servers)), null)
- Return: $key!=$state
checkServerNeedsConfiguration:
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
Body:
- $key: $this.getKey()
- $state: $this.getAttr(format('configuration_of_server_{0}', id($server)), null)
- Return: $key!=$state
getKey:
Body:
# should be redefined in subclasses to contain semantical signature
# of the object's configuration
- Return: $this._randomName
preConfigure:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $this.report(format('Applying configuration of {0}', $this.componentName))
- $this.onPreConfigure($servers)
onPreConfigure:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $sr: $this.getSecurityRules()
- If: $sr
Then:
- $this._env.securityGroupManager.addGroupIngress($sr)
- $this._env.stack.push()
- $this.preConfigureEvent.notify($this, $servers)
configureServer:
Meta:
- m:Synchronize:
onArgs: server
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
Body:
- Try:
- $this.report(format('Began configuring {0} on {1}',
$this.componentName, $server.name))
- $this.onConfigureServer($server)
Catch:
- As: e
Do:
- $this.report(format('Unable to configure {0} on {1} due to {2}',
$this.componentName, $server.name, $e.message))
- Return: False
Else:
- $key: $this.getKey()
- $this.setAttr(format('configuration_of_server_{0}', id($server)), $key)
- $this.report(format('{0} is configured at {1}',
$this.componentName, $server.name))
- Return: True
onConfigureServer:
Meta:
- m:Synchronize:
onArgs: server
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
Body:
- $this.configureServerEvent.notify($this, $server)
completeConfiguration:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
- numFailures:
Contract: $.int()
Body:
- $success: $this._detect_success($servers.items.len(), $numFailures)
- If: $success
Then:
- $this.onCompleteConfiguration($servers)
- $key: $this.getKey()
- $this.setAttr(format('configuration_of_cluster_{0}', id($servers)),
$key)
- $this.report(format('Finished configuring {0} ({1} errors encountered)',
$this.componentName, $numFailures or 'no'))
Else:
- Throw: TooManyConfigurationErrors
Message: format('Too many errors ({0}) encountered while configuring {1}',
$numFailures, $this.componentName)
onCompleteConfiguration:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- $this.completeConfigurationEvent.notify($this, $servers)
getSecurityRules:
Body:
- Return: {}
deployAt:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- super($this, $.deployAt($servers))
- $this.configure($servers)
--- # ------------------------------------------------------------------ # ---
Name: SoftwareComponent
Extends:
- Installable
- Configurable
Methods:
deployAt:
Arguments:
- servers:
Contract: $.class(InstanceGroup).notNull()
Body:
- cast($this, Installable).deployAt($servers)
- cast($this, Configurable).deployAt($servers)

View File

@ -34,20 +34,38 @@ Properties:
- $.class(res:Instance)
Methods:
.init:
Body:
- $env: $.find(std:Environment)
- If: $env
Then:
- $this._reporter: $env.reporter
Else:
- $this._reporter: null
deploy:
Body:
- cast($this, ReplicationGroup).deploy()
- $this.items.select($.beginDeploy())
- $this.items.select($this._deployInstance($))
- $this.items.select($.endDeploy())
_deployInstance:
Arguments:
- instance:
Contract: $.class(res:Instance)
Body:
- If: $this._reporter and name($instance) and not $instance.openstackId
Then:
- $this._reporter.report($this, 'Provisioning VM for ' + name($instance))
- $instance.beginDeploy()
.destroy:
Body:
- $this.items.select($.beginReleaseResources())
- $this.items.select($.endReleaseResources())
--- # ------------------------------------------------------------------ # ---
# A replication provider acting as a default factory class for
# LinuxMuranoInstance (LMI)
# A replication provider acting as a default factory class for Instances
Name: InstanceProvider
Extends: CloneReplicaProvider

View File

@ -0,0 +1,106 @@
Namespaces:
=: io.murano.applications.tests
tst: io.murano.test
apps: io.murano.applications
res: io.murano.resources
--- # ------------------------------------------------------------------ # ---
Name: InstallableToTest
Extends:
- apps:Installable
--- # ------------------------------------------------------------------ # ---
Name: ConfigurableToTest
Extends:
- apps:Configurable
--- # ------------------------------------------------------------------ # ---
Name: SoftwareComponentToTest
Extends:
- apps:SoftwareComponent
--- # ------------------------------------------------------------------ # ---
Name: TestSoftwareComponent
Extends: tst:TestFixtureWithEnvironment
Properties:
reports:
Contract:
- $.string()
Usage: Out
Default: []
Methods:
report:
Arguments:
- instance:
Contract: $
- message:
Contract: $.string()
Body:
- $this.reports: $this.reports.append($message)
setUp:
Body:
- super($this, $.setUp())
- inject(res:LinuxMuranoInstance, beginDeploy, '')
- inject(res:LinuxMuranoInstance, endDeploy, '')
- $instance: new(res:LinuxMuranoInstance, $this.environment,
name=>'noop',
image=>'noop',
flavor=>'noop')
- $provider: new(apps:InstanceProvider, $this.environment,
source=>$instance, instanceNamePattern=>'testNode-{0}')
- $this.group: new(apps:InstanceGroup, provider=>$provider, numItems=>5)
- $this.group.deploy()
- $this.reports: []
- inject($this.environment.reporter, report, $this, report)
testInstallReportingSequence:
Body:
- $cmp: new(InstallableToTest, $this.environment, testComp)
- $cmp.deployAt($this.group)
- $this.assertInstallingSequence(0)
testConfigureReportingSequence:
Body:
- $cmp: new(ConfigurableToTest, $this.environment, testComp)
- $cmp.deployAt($this.group)
- $this.assertConfiguringSequence(0)
testCombinedSequence:
Body:
- $cmp: new(SoftwareComponentToTest, $this.environment, testComp)
- $cmp.deployAt($this.group)
- $this.assertInstallingSequence(0)
- $this.assertConfiguringSequence(2*$this.group.numItems+2)
assertInstallingSequence:
Arguments:
- offset:
Contract: $.int().notNull()
Body:
- $this.assertEqual('Installing testComp', $this.reports[$offset])
- $nodeReports: range(0, $this.group.numItems*2).select($this.reports[$offset+1+$])
- range(1, $this.group.numItems+1).select(
$this.assertTrue(format('Began installing testComp on testNode-{0}', $) in $nodeReports))
- range(1, $this.group.numItems+1).select(
$this.assertTrue(format('testComp is installed on testNode-{0}', $) in $nodeReports))
- $this.assertEqual('Finished installing testComp (no errors encountered)', $this.reports[$offset+2*$this.group.numItems+1])
assertConfiguringSequence:
Arguments:
- offset:
Contract: $.int().notNull()
Body:
- $this.assertEqual('Applying configuration of testComp', $this.reports[$offset+0])
- $nodeReports: range(0, $this.group.numItems*2).select($this.reports[$offset+1+$])
- range(1, $this.group.numItems+1).select(
$this.assertTrue(format('Began configuring testComp on testNode-{0}', $) in $nodeReports))
- range(1, $this.group.numItems+1).select(
$this.assertTrue(format('testComp is configured at testNode-{0}', $) in $nodeReports))
- $this.assertEqual('Finished configuring testComp (no errors encountered)', $this.reports[$offset+2*$this.group.numItems+1])

View File

@ -34,6 +34,12 @@ Classes:
io.murano.applications.InstanceGroup: instance.yaml
io.murano.applications.InstanceProvider: instance.yaml
io.murano.applications.BaseSoftwareComponent: component.yaml
io.murano.applications.Installable: component.yaml
io.murano.applications.Configurable: component.yaml
io.murano.applications.SoftwareComponent: component.yaml
# Tests
io.murano.applications.tests.TestReplication: tests/TestReplication.yaml
io.murano.applications.tests.TestCloneReplicaProvider: tests/TestCloneReplicaProvider.yaml
@ -42,3 +48,4 @@ Classes:
io.murano.applications.tests.NestedReplicationTarget: tests/TestTemplateReplicaProvider.yaml
io.murano.applications.tests.TestEvents: tests/TestEvents.yaml
io.murano.applications.tests.TestMockedInstanceFactory: tests/TestInstanceProviders.yaml
io.murano.applications.tests.TestSoftwareComponent: tests/TestSoftwareComponent.yaml