Add provider and type to deploy workflows
The new type defines a minimal workflow object and the provider allows the creation/update/deletion of a workflow. Since the creation command is a little different (because you can create multiple workflows from a single definition file), the provider inherits from an intermediate class that only overrides the creation action. Change-Id: Ia22cfc031915fa56cdc2602a5746b2250a62963b
This commit is contained in:
parent
5f39db147c
commit
cc27c46f99
42
README.md
42
README.md
@ -78,6 +78,48 @@ Whether to hide the value from Puppet logs. Defaults to `false`.
|
||||
|
||||
If value is equal to ensure_absent_val then the resource will behave as if `ensure => absent` was specified. Defaults to `<SERVICE DEFAULT>`
|
||||
|
||||
### mistral_workflow
|
||||
|
||||
The `mistral_workflow` provider allows the creation/update/deletion of workflow definitions using a source file (in YAML).
|
||||
|
||||
```puppet
|
||||
mistral_workflow { 'my_workflow':
|
||||
ensure => present,
|
||||
definition_file => '/home/user/my_workflow.yaml',
|
||||
is_public => true,
|
||||
}
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```puppet
|
||||
mistral_workflow { 'my_workflow':
|
||||
ensure => absent,
|
||||
}
|
||||
```
|
||||
|
||||
If you need to force the update of the workflow or change it's public attribute, use `latest`:
|
||||
```puppet
|
||||
mistral_workflow { 'my_workflow':
|
||||
ensure => latest,
|
||||
definition_file => '/home/user/my_workflow.yaml',
|
||||
is_public => false,
|
||||
}
|
||||
```
|
||||
|
||||
Although the mistral client allows multiple workflow definitions per source file, it not recommended to do so with this provider as the `mistral_workflow` is supposed to represent a single workflow.
|
||||
|
||||
#### name
|
||||
|
||||
The name of the workflow; this is only used when deleting the workflow since the definition file specifies the name of the workflow to create/update.
|
||||
|
||||
#### definition_file
|
||||
|
||||
The path to the file containing the definition of the workflow. This parameter is not mandatory but the creation or update will fail if it is not supplied.
|
||||
|
||||
#### is_public
|
||||
|
||||
Specifies whether the workflow must be public or not. Defaults to `true`.
|
||||
|
||||
Beaker-Rspec
|
||||
------------
|
||||
|
106
lib/puppet/provider/mistral.rb
Normal file
106
lib/puppet/provider/mistral.rb
Normal file
@ -0,0 +1,106 @@
|
||||
require 'puppet/util/inifile'
|
||||
require 'puppet/provider/openstack/auth'
|
||||
require 'puppet/provider/openstack/credentials'
|
||||
require File.join(File.dirname(__FILE__), '..','..', 'puppet/provider/mistral_workflow_requester')
|
||||
|
||||
class Puppet::Provider::Mistral < Puppet::Provider::MistralWorkflowRequester
|
||||
|
||||
extend Puppet::Provider::Openstack::Auth
|
||||
|
||||
def self.request(service, action, properties=nil)
|
||||
begin
|
||||
super
|
||||
rescue Puppet::Error::OpenstackAuthInputError, Puppet::Error::OpenstackUnauthorizedError => error
|
||||
mistral_request(service, action, error, properties)
|
||||
end
|
||||
end
|
||||
|
||||
def self.mistral_request(service, action, error, properties=nil)
|
||||
properties ||= []
|
||||
@credentials.username = mistral_credentials['admin_username']
|
||||
@credentials.password = mistral_credentials['admin_password']
|
||||
@credentials.project_name = mistral_credentials['admin_project_name']
|
||||
@credentials.auth_url = auth_endpoint
|
||||
if mistral_credentials['region_name']
|
||||
@credentials.region_name = mistral_credentials['region_name']
|
||||
end
|
||||
if @credentials.version == '3'
|
||||
@credentials.user_domain_name = mistral_credentials['user_domain_name']
|
||||
@credentials.project_domain_name = mistral_credentials['project_domain_name']
|
||||
end
|
||||
raise error unless @credentials.set?
|
||||
|
||||
if action == 'create'
|
||||
mistral_create_request(action, properties)
|
||||
else
|
||||
Puppet::Provider::Openstack.request(service, action, properties, @credentials)
|
||||
end
|
||||
end
|
||||
|
||||
def self.conf_filename
|
||||
'/etc/mistral/mistral.conf'
|
||||
end
|
||||
|
||||
def self.mistral_conf
|
||||
return @mistral_conf if @mistral_conf
|
||||
@mistral_conf = Puppet::Util::IniConfig::File.new
|
||||
@mistral_conf.read(conf_filename)
|
||||
@mistral_conf
|
||||
end
|
||||
|
||||
def self.mistral_credentials
|
||||
@mistral_credentials ||= get_mistral_credentials
|
||||
end
|
||||
|
||||
def mistral_credentials
|
||||
self.class.mistral_credentials
|
||||
end
|
||||
|
||||
def self.get_mistral_credentials
|
||||
auth_keys = ['auth_uri', 'admin_tenant_name', 'admin_user',
|
||||
'admin_password']
|
||||
conf = mistral_conf
|
||||
if conf and conf['keystone_authtoken'] and
|
||||
auth_keys.all?{|k| !conf['keystone_authtoken'][k].nil?}
|
||||
creds = Hash[ auth_keys.map \
|
||||
{ |k| [k, conf['keystone_authtoken'][k].strip] } ]
|
||||
|
||||
if conf['project_domain_name']
|
||||
creds['project_domain_name'] = conf['project_domain_name']
|
||||
else
|
||||
creds['project_domain_name'] = 'Default'
|
||||
end
|
||||
|
||||
if conf['user_domain_name']
|
||||
creds['user_domain_name'] = conf['user_domain_name']
|
||||
else
|
||||
creds['user_domain_name'] = 'Default'
|
||||
end
|
||||
|
||||
if conf['region_name']
|
||||
creds['region_name'] = conf['region_name']
|
||||
end
|
||||
return creds
|
||||
else
|
||||
raise(Puppet::Error, "File: #{conf_filename} does not contain all " +
|
||||
"required authentication options. Mistral types will not work " +
|
||||
"if mistral is not correctly configured to use Keystone " +
|
||||
"authentication.")
|
||||
end
|
||||
end
|
||||
|
||||
def self.get_auth_endpoint
|
||||
m = mistral_credentials
|
||||
"#{m['auth_uri']}"
|
||||
end
|
||||
|
||||
def self.auth_endpoint
|
||||
@auth_endpoint ||= get_auth_endpoint
|
||||
end
|
||||
|
||||
def self.reset
|
||||
@mistral_conf = nil
|
||||
@mistral_credentials = nil
|
||||
end
|
||||
|
||||
end
|
90
lib/puppet/provider/mistral_workflow/openstack.rb
Normal file
90
lib/puppet/provider/mistral_workflow/openstack.rb
Normal file
@ -0,0 +1,90 @@
|
||||
require File.join(File.dirname(__FILE__), '..','..','..', 'puppet/provider/mistral')
|
||||
|
||||
Puppet::Type.type(:mistral_workflow).provide(
|
||||
:openstack,
|
||||
:parent => Puppet::Provider::Mistral
|
||||
) do
|
||||
desc <<-EOT
|
||||
Mistral provider to manage workflow type
|
||||
EOT
|
||||
|
||||
@credentials = Puppet::Provider::Openstack::CredentialsV3.new
|
||||
|
||||
mk_resource_methods
|
||||
|
||||
def create
|
||||
properties = []
|
||||
properties << (@resource[:is_public] == :true ? '--public' : '--private')
|
||||
properties << @resource[:definition_file]
|
||||
|
||||
self.class.request('workflow', 'create', properties)
|
||||
@property_hash[:ensure] = :present
|
||||
@property_hash[:definition_file] = resource[:definition_file]
|
||||
@property_hash[:is_public] = resource[:is_public]
|
||||
@property_hash[:name] = name
|
||||
end
|
||||
|
||||
def exists?
|
||||
@property_hash[:ensure] == :present
|
||||
end
|
||||
|
||||
def destroy
|
||||
self.class.request('workflow', 'delete', @resource[:name])
|
||||
@property_hash.clear
|
||||
end
|
||||
|
||||
def update
|
||||
# Update the workflow if it exists, otherwise create it
|
||||
if exists?
|
||||
properties = []
|
||||
if @resource[:is_public] == :true
|
||||
properties << '--public'
|
||||
end
|
||||
properties << @resource[:definition_file]
|
||||
|
||||
self.class.request('workflow', 'update', properties)
|
||||
@property_hash[:ensure] = :present
|
||||
@property_hash[:definition_file] = resource[:definition_file]
|
||||
@property_hash[:is_public] = resource[:is_public]
|
||||
@property_hash[:name] = name
|
||||
else
|
||||
create
|
||||
end
|
||||
end
|
||||
|
||||
def self.instances
|
||||
list = request('workflow', 'list')
|
||||
list.collect do |wf|
|
||||
attrs = request('workflow', 'show', wf[:id])
|
||||
new({
|
||||
:ensure => :present,
|
||||
:id => wf[:id],
|
||||
:name => wf[:name],
|
||||
:is_public => (attrs[:scope] == "public")
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def self.prefetch(resources)
|
||||
workflows = instances
|
||||
resources.keys.each do |name|
|
||||
if provider = workflows.find{ |wf| wf.name == name }
|
||||
resources[name].provider = provider
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def flush
|
||||
if @property_flush
|
||||
opts = [@resource[:name]]
|
||||
|
||||
(opts << '--public') if @property_flush[:is_public] == :true
|
||||
(opts << '--private') if @property_flush[:is_public] == :false
|
||||
opts << @property_flush[:definition_file]
|
||||
|
||||
self.class.request('workflow', 'update', opts)
|
||||
@property_flush.clear
|
||||
end
|
||||
end
|
||||
|
||||
end
|
64
lib/puppet/provider/mistral_workflow_requester.rb
Normal file
64
lib/puppet/provider/mistral_workflow_requester.rb
Normal file
@ -0,0 +1,64 @@
|
||||
require 'csv'
|
||||
require 'puppet'
|
||||
require 'timeout'
|
||||
|
||||
class Puppet::Provider::MistralWorkflowRequester < Puppet::Provider::Openstack
|
||||
# This class only overrides the request method of the Openstack provider
|
||||
# because Mistral behaves differently when creating workflows.
|
||||
# The mistral client allows the creation of multiple workflows with a single
|
||||
# definition file so the creation call returns a list of workflows instead of
|
||||
# a single value.
|
||||
# Consequently the shell output format is not available and the csv formatter
|
||||
# must be used instead.
|
||||
|
||||
# Returns an array of hashes, where the keys are the downcased CSV headers
|
||||
# with underscores instead of spaces
|
||||
#
|
||||
# @param options [Hash] Other options
|
||||
# @options :no_retry_exception_msgs [Array<Regexp>,Regexp] exception without retries
|
||||
def self.request(service, action, properties, credentials=nil, options={})
|
||||
env = credentials ? credentials.to_env : {}
|
||||
|
||||
# We only need to override the create action
|
||||
if action != 'create'
|
||||
return super
|
||||
end
|
||||
|
||||
Puppet::Util.withenv(env) do
|
||||
rv = nil
|
||||
begin
|
||||
# shell output is:
|
||||
# ID,Name,Description,Enabled
|
||||
response = openstack(service, action, '--quiet', '--format', 'csv', properties)
|
||||
response = parse_csv(response)
|
||||
keys = response.delete_at(0)
|
||||
|
||||
if response.collect.length > 1
|
||||
definition_file = properties[-1]
|
||||
Puppet.warning("#{definition_file} creates more than one workflow, only the first one will be returned after the request.")
|
||||
end
|
||||
rv = response.collect do |line|
|
||||
hash = {}
|
||||
keys.each_index do |index|
|
||||
key = keys[index].downcase.gsub(/ /, '_').to_sym
|
||||
hash[key] = line[index]
|
||||
end
|
||||
hash
|
||||
end
|
||||
rescue Puppet::ExecutionFailure => exception
|
||||
raise Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate' if exception.message =~ /HTTP 40[13]/
|
||||
raise
|
||||
end
|
||||
end
|
||||
return rv
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.parse_csv(text)
|
||||
# Ignore warnings - assume legitimate output starts with a double quoted
|
||||
# string. Errors will be caught and raised prior to this
|
||||
text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
|
||||
return CSV.parse(text + "\n")
|
||||
end
|
||||
end
|
63
lib/puppet/type/mistral_workflow.rb
Normal file
63
lib/puppet/type/mistral_workflow.rb
Normal file
@ -0,0 +1,63 @@
|
||||
Puppet::Type.newtype(:mistral_workflow) do
|
||||
desc <<-EOT
|
||||
This allows manifests to declare a workflow to be created or removed
|
||||
in Mistral.
|
||||
|
||||
mistral_workflow { "my_workflow":
|
||||
ensure => present,
|
||||
definition_file => "/home/workflows/my_workflow.yaml",
|
||||
is_public => yes,
|
||||
}
|
||||
|
||||
Known problems / limitations:
|
||||
* When creating a worflow, the name supplied is not used because mistral
|
||||
will name the workflow according to its definition.
|
||||
* You MUST provide the definition_file if you want to change any property
|
||||
because that will cause the provider to run the 'workflow update'
|
||||
command.
|
||||
* DO NOT put multiple workflows in the definition_file. Although the
|
||||
mistral client allows it, the provider does not support it.
|
||||
* Ensure this is run on the same server as the mistral-api service.
|
||||
|
||||
EOT
|
||||
|
||||
ensurable do
|
||||
newvalue(:present) do
|
||||
provider.create
|
||||
end
|
||||
|
||||
newvalue(:absent) do
|
||||
provider.destroy
|
||||
end
|
||||
|
||||
newvalue(:latest) do
|
||||
provider.update
|
||||
end
|
||||
end
|
||||
|
||||
newparam(:name, :namevar => true) do
|
||||
desc 'The name of the workflow'
|
||||
newvalues(/.*/)
|
||||
end
|
||||
|
||||
newproperty(:id) do
|
||||
desc 'The unique id of the workflow'
|
||||
newvalues(/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/)
|
||||
end
|
||||
|
||||
newparam(:definition_file) do
|
||||
desc "The location of the file defining the workflow"
|
||||
newvalues(/.*/)
|
||||
end
|
||||
|
||||
newparam(:is_public, :boolean => true) do
|
||||
desc 'Whether the workflow is public or not. Default to `true`'
|
||||
newvalues(:true, :false)
|
||||
defaultto true
|
||||
end
|
||||
|
||||
# Require the Mistral API service to be running
|
||||
autorequire(:service) do
|
||||
['mistral-api']
|
||||
end
|
||||
end
|
1
tests/workflow.pp
Normal file
1
tests/workflow.pp
Normal file
@ -0,0 +1 @@
|
||||
class { '::mistral:workflow': }
|
Loading…
x
Reference in New Issue
Block a user