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