diff --git a/README.md b/README.md index df3f819..f991c6e 100644 --- a/README.md +++ b/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 `` +### 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 ------------ diff --git a/lib/puppet/provider/mistral.rb b/lib/puppet/provider/mistral.rb new file mode 100644 index 0000000..b23be84 --- /dev/null +++ b/lib/puppet/provider/mistral.rb @@ -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 diff --git a/lib/puppet/provider/mistral_workflow/openstack.rb b/lib/puppet/provider/mistral_workflow/openstack.rb new file mode 100644 index 0000000..55e5a95 --- /dev/null +++ b/lib/puppet/provider/mistral_workflow/openstack.rb @@ -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 diff --git a/lib/puppet/provider/mistral_workflow_requester.rb b/lib/puppet/provider/mistral_workflow_requester.rb new file mode 100644 index 0000000..ac2f54f --- /dev/null +++ b/lib/puppet/provider/mistral_workflow_requester.rb @@ -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] 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 diff --git a/lib/puppet/type/mistral_workflow.rb b/lib/puppet/type/mistral_workflow.rb new file mode 100644 index 0000000..4c85978 --- /dev/null +++ b/lib/puppet/type/mistral_workflow.rb @@ -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 diff --git a/tests/workflow.pp b/tests/workflow.pp new file mode 100644 index 0000000..f9a70fe --- /dev/null +++ b/tests/workflow.pp @@ -0,0 +1 @@ +class { '::mistral:workflow': }