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:
Pierre Gaxatte 2018-06-07 15:47:25 +02:00
parent 5f39db147c
commit cc27c46f99
6 changed files with 366 additions and 0 deletions

View File

@ -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
------------

View 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

View 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

View 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

View 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
View File

@ -0,0 +1 @@
class { '::mistral:workflow': }