From ad0718dfd29e58759d87b130e373ed8ac2373200 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Sat, 30 Nov 2019 16:57:48 +0100 Subject: [PATCH] Add new providers Adds the new base code for talking directly to OpenStack APIs for the provider resources. Change-Id: I4c989ddec2d3f14e91d7f4596bf9a0baf809fc57 --- lib/puppet/resource_api/openstack_provider.rb | 95 +++++++ lib/puppet_x/openstack/client_base.rb | 169 +++++++++++ lib/puppet_x/openstack/credentials.rb | 232 +++++++++++++++ lib/puppet_x/openstack/http_base.rb | 206 ++++++++++++++ lib/puppet_x/openstack/resource.rb | 267 ++++++++++++++++++ lib/puppet_x/openstack/token_request.rb | 101 +++++++ 6 files changed, 1070 insertions(+) create mode 100644 lib/puppet/resource_api/openstack_provider.rb create mode 100644 lib/puppet_x/openstack/client_base.rb create mode 100644 lib/puppet_x/openstack/credentials.rb create mode 100644 lib/puppet_x/openstack/http_base.rb create mode 100644 lib/puppet_x/openstack/resource.rb create mode 100644 lib/puppet_x/openstack/token_request.rb diff --git a/lib/puppet/resource_api/openstack_provider.rb b/lib/puppet/resource_api/openstack_provider.rb new file mode 100644 index 00000000..c724b4c0 --- /dev/null +++ b/lib/puppet/resource_api/openstack_provider.rb @@ -0,0 +1,95 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet' +require 'puppet/resource_api/simple_provider' + +class Puppet::ResourceApi::OpenStackProvider < Puppet::ResourceApi::SimpleProvider + # @method set + # Override set so that we can update the context. + # @param context + # The context. + # @param changes + # The changes. + def set(context, changes) + # Name it openstack_changes so that it never collides with any Puppet naming + # if they introduce anything in the future that is named "changes". + class << context + attr_accessor :openstack_changes + + # @method get_openstack_change + # Get the change by name from our openstack_changes. + # @param name + # The name of the resource. + # @return hash + # Returns the hash with the :is and :should keys set that + # conains all the data for the resource. All data returned + # in those keys are symbols. + def get_openstack_change(name) + if @openstack_changes.key?(name.to_s) + @openstack_changes[name.to_s] + else + nil + end + end + + # @method get_openstack_is_id + # Get the resource ID from the "is" data. + # @param name + # The name of the resource. + # @return string or nil + # The string with the ID or nil. + def get_openstack_is_id(name) + change = get_openstack_change(name) + + if change && change.key?(:is) && change[:is].key?(:id) + change[:is][:id] + else + nil + end + end + end + + context.openstack_changes = changes + super + end + + # @method handle_deprecations + # Handle the custom deprecations field in the type defintion. + # @param context + # The Puppet::ResourceApi::BaseContext context. + def handle_deprecations(context) + if context.type.definition.key?(:deprecations) + deprecations = context.type.definition[:deprecations] + + if !deprecations.is_a?(Array) + raise Puppet::ResourceError 'Deprecations must be an array of strings' + end + + deprecations.each do |d| + context.warning("The '#{d.to_s}' parameter is deprecated") + end + end + end + + # @method get + # The get function for the provider. + # @param context + # The Puppet::ResourceApi::BaseContext context. + def get(context) + handle_deprecations(context) + end +end diff --git a/lib/puppet_x/openstack/client_base.rb b/lib/puppet_x/openstack/client_base.rb new file mode 100644 index 00000000..b817651e --- /dev/null +++ b/lib/puppet_x/openstack/client_base.rb @@ -0,0 +1,169 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet' +require 'puppet_x' +require 'json' +require 'puppet_x/openstack/http_base' + +module PuppetX::OpenStack + # Client base class that is used to provide the base + # functionality for all other clients. + class ClientBase < HttpBase + @creds = nil + @@cached_token = nil + @@cached_catalog = nil + + # @method initialize + # Initialize the class. + # @param credentials + # The credentials to use. + # @param get_token_on_instance + # Boolean if we should try to get a token on instantiation. + def initialize(credentials, get_token_on_instance: true) + @creds = credentials + + # Initialize HttpBase with the auth_url as base url. + # We need to do this first so that we can issue a token. + super(@creds.auth_url) + + # Get a token on initialize if it was requested. + token if get_token_on_instance + + # If we have a service type lets lookup the endpoint and + # update the base url. This will generate a warning and + # set the base_url to nil if we don't create a token on + # instantiation (see get_token_on_instance) which means + # the client class that uses this base is responsible for + # setting the base url upon initialization. + if @creds.service_type + @base_url = endpoint(@creds.service_type) + end + end + + # @method token + # Authenticate for a token and cache it. + # @return string + # An authenticated token. + def token + return @@cached_token if @@cached_token + + req = TokenRequest.new(@creds) + body = req.body + + response = request('POST', '/v3/auth/tokens', body.to_json) + + if response.code.to_i != 201 + raise Puppet::ResourceError, "Failed to authenticate, got code #{response.code.to_i} from Keystone" + end + + response_body = JSON.parse(response.body) + + if response_body['token'] and response_body['token']['catalog'] + @@cached_catalog = response_body['token']['catalog'] + end + + @@cached_token = response['X-Subject-Token'] + + return @@cached_token + end + + # @method revoke + # Revoke the cached token. + # @return boolean + # The boolean is true on success otherwise false. + def revoke + return false if not @@cached_token + + headers = { + 'X-Auth-Token': @@cached_token, + 'X-Subject-Token': @@cached_token, + } + + response = request('DELETE', '/v3/auth/tokens', nil, headers) + return response.code.to_i == 204 + end + + # @method endpoint + # Lookup an endpoint in the cached catalog. + # Note that the authenticated token must have included + # a catalog otherwise it will not be available. + # @param service_type + # The service type to find endpoint for. + # @param region_name + # The region that the service type should be in. + # This parameter can be nil and it will use the region + # name from the credentials. Fallback to RegionOne. + # @param interface + # The interface to use for the endpoint. + # This parameter can be nil and it will use the interface + # from the credentials. Fallback to public interface. + # @return string or nil + # The endpoint found in the catalog or nil. + # @raises Puppet::ResourceError + # When no region name is specified to this function or in + # the credentials. + def endpoint(service_type, region_name=nil, interface=nil) + return nil if not @@cached_catalog + + if region_name + lookup_region = region_name + elsif @creds.region_name + lookup_region = @creds.region_name + else + Puppet.debug("Failing back to RegionOne as region when looking up service #{service_type}") + lookup_region = 'RegionOne' + end + + if interface + lookup_interface = interface + elsif @creds.interface + lookup_interface = @creds.interface + else + Puppet.debug("Failing back to public interface when looking up service #{service_type} in region #{lookup_region}") + lookup_interface = 'public' + end + + @@cached_catalog.each do |service| + next if service['type'] != service_type + + service['endpoints'].each do |ep| + if ep['region'] == lookup_region and ep['interface'] == lookup_interface + Puppet.debug("Found endpoint #{ep['url']} for service #{service_type} in region #{lookup_region}") + return ep['url'] + end + end + end + + Puppet.warning("Did not find a endpoint for service #{service_type} in region #{lookup_region} will return nil") + nil + end + + # @method auth_header + # Get hash with headers for authenticated requests. + # @return hash + # A hash with the headers. + # @raises Puppet::ResourceError + # When a token has not been authenticated yet. + def auth_header + if not @@cached_token + raise Puppet::ResourceError, "Cannot generate authenticated headers because no token has been created yet" + end + + { 'X-Auth-Token': @@cached_token } + end + end +end diff --git a/lib/puppet_x/openstack/credentials.rb b/lib/puppet_x/openstack/credentials.rb new file mode 100644 index 00000000..6ecf5c9b --- /dev/null +++ b/lib/puppet_x/openstack/credentials.rb @@ -0,0 +1,232 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet' +require 'puppet_x' +require 'puppet/util/inifile' +require 'uri' + +module PuppetX::OpenStack + # This class stores credentials that can be reused by all other + # classes that need access to a wide variety of them. + class Credentials + # The credentials that are available, note that these need + # to be the same as named in the keystone_authtoken section. + KEYS = [ + :auth_url, + :scope, + :user_id, + :username, + :password, + :project_id, + :project_name, + :domain_id, + :domain_name, + :user_domain_id, + :user_domain_name, + :project_domain_id, + :project_domain_name, + :region_name, + :interface, + :service_type, + ] + + KEYS.each { |var| attr_accessor var } + + # @method initialize + # Initialize the credentials class. + def initialize + # Default to project scope that can then be changed + # if something else is required. + @scope = 'project' + end + + # @method from_keystone_authtoken + # Reads the credentials from the keystone_authtoken section in a config file. + # @param file The configuration file to read the section from. + def from_keystone_authtoken(file) + begin + conf = read_config(file) + rescue => e + raise Puppet::ResourceError, "Failed to read config #{file}: #{e.message}" + end + + return if not conf or not conf['keystone_authtoken'] + + KEYS.each do |k| + if not conf['keystone_authtoken'][k.to_s].nil? + self.instance_variable_set("@#{k.to_s}", conf['keystone_authtoken'][k.to_s]) + end + end + + # If we dont get a auth_url from the config lets fallback to the + # www_authenticate_uri option if it exists and try that. + if not @auth_url and conf['keystone_authtoken']['www_authenticate_uri'] + @auth_url = conf['keystone_authtoken']['www_authenticate_uri'] + end + + if validate_auth_url? && sanitize_auth_url? + Puppet.debug("Credentials auth URL is valid but was sanitized to: #{@auth_url}") + end + end + + # @method validate + # Validate that the credentials that are stored right now can be used + # and that the required credentials for the specified scope is available. + # @raises Puppet::ResourceError + # When a credentials is invalid. + def validate + if not @auth_url + raise Puppet::ResourceError, 'Credentials must have a auth URL' + end + + if not validate_auth_url? + raise Puppet::ResourceError, "The auth URL in credentials is an invalid HTTP URL: #{@auth_url}" + end + + if not @user_id and not @username + raise Puppet::ResourceError, 'Credentials must have a user ID or username' + end + + if not @password + raise Puppet::ResourceError, 'Credentials must have a password' + end + + if not ['unscoped', 'system', 'domain', 'project'].include? @scope + raise Puppet::ResourceError, "Credentials contains an invalid scope: #{@scope}" + end + + if not @user_domain_id and not @user_domain_name + raise Puppet::ResourceError, 'Credentials must have a user domain ID or user domain name' + end + + if @scope == 'domain' and (not @domain_id and not @domain_name) + raise Puppet::ResourceError, 'Credentials with domain scope must have a domain ID or domain name' + end + + if @scope == 'project' and (not @project_id and not @project_name) + raise Puppet::ResourceError, 'Credentials with project scope must have project ID or project name' + end + + if @scope == 'project' and (not @project_domain_id and not @project_domain_name) + raise Puppet::ResourceError, 'Credentials with project scope must have a project domain ID or project domain name' + end + + if @interface and not ['internal', 'admin', 'public'].include? @interface + raise Puppet::ResourceError, "Credentials interface must be internal, admin or public, got: #{@interface}" + end + + # TODO: Validate service_type here. + # If the service_type is not set we will talk to the auth_url i.e keystone. + end + + # @method user + # Get the user ID or name. + def user + get_id_or_name(@user_id, @username) + end + + # @method user_domain + # Get the user domain ID or name. + # @return hash + # With the ID or name set. + def user_domain + get_id_or_name(@user_domain_id, @user_domain_name) + end + + # @method domain + # Get the domain ID or name. + # @return hash + # With the ID or name set. + def domain + get_id_or_name(@domain_id, @domain_name) + end + + # @method project + # Get the project ID or name. + # @return hash + # With the ID or name set. + def project + get_id_or_name(@project_id, @project_name) + end + + # @method project_domain + # Get the project domain ID or name. + # @return hash + # With the ID or name set. + def project_domain + get_id_or_name(@project_domain_id, @project_domain_name) + end + + private + + # @method validate_auth_url? + # Validate the auth_url is a valid HTTP URL. + # @return boolean + # That is true on success otherwise false. + def validate_auth_url? + url = URI.parse(@auth_url) + url.is_a?(URI::HTTP) && !url.host.nil? + rescue URI::InvalidURIError + false + end + + # @method sanitize_auth_url? + # Sanitize the auth_url by removing the path. + # @return boolean + # Returns true if the auth_url was sanitized otherwise false. + def sanitize_auth_url? + changed = false + url = URI.parse(@auth_url) + if url.path != '' or url.query != nil + url.path = '' + url.query = nil + changed = true + end + @auth_url = url.to_s + changed + end + + # @method get_id_or_name + # Get the ID or name depending on which one of them + # is not nil. + # @return hash + # With the ID or name set. + def get_id_or_name(id, name) + if not id and not name + return nil + end + result = {} + if id + result[:id] = id + else + result[:name] = name + end + result + end + + # @method read_config + # Open config file, read it and save data. + # @return hash + # The read contents from the config file. + def read_config(file) + return @config if @config + @config = Puppet::Util::IniConfig::File.new + @config.read(file) + @config + end + end +end diff --git a/lib/puppet_x/openstack/http_base.rb b/lib/puppet_x/openstack/http_base.rb new file mode 100644 index 00000000..5fa67247 --- /dev/null +++ b/lib/puppet_x/openstack/http_base.rb @@ -0,0 +1,206 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet' +require 'puppet_x' +require 'net/http' + +module PuppetX::OpenStack + class HttpBase + @base_url = nil + @default_headers = nil + @ca_file = nil + + # @method initialize + # Initialize the class. + # @param base_url + # The base url to use. + # @param default_headers + # A hash with default headers to use. + def initialize(base_url, default_headers={}) + @base_url = base_url + @default_headers = default_headers + + # This is not optimal but we must lookup where we should + # load our trusted CAs from. The best way would be to use + # the system path provided by OpenSSL::X509::DEFAULT_CERT_DIR + # but since Puppet runs it's own Ruby stack that points + # to /opt/puppetlabs/puppet/ssl/certs which is not where + # all trusted or self-signed CAs would be placed. What we + # do instead is we lookup the default trusted CA files that + # the operating systems provide. + ca_files = ['/etc/pki/tls/cert.pem', '/etc/ssl/certs/ca-certificates.crt'] + + @ca_file = ca_files.find { |f| + File.file?(f) + } + + if not @ca_files + Puppet.warning("Could not find any of the CA files: #{ca_files.inspect}") + end + end + + protected + + # @method parse_url + # Parse the base URL and fix the path that will be used for the requests. + # Protected function that can only used by this class and subclasses that + # inherits this class. + # @param path + # The path to add to the base URL. + # @param query + # Optional parameter if the query parameters that should be added to + # the URL. + # @return string + # The generated full URL with the specified path and query. + def parse_url(path, query=nil) + url = URI.parse(@base_url) + path.prepend('/') unless path[0] == '/' + url.path += path + url.query = query if query + url + end + + # @method default_headers + # Get all default headers. + # @param array + # An array with all default headers. + def default_headers + @default_headers + end + + # @method default_headers= + # Set all default headers. + # @param value + # An array with all default headers. + def default_headers=(value) + @default_headers = value + end + + # @method default_header= + # Set one default header. + # @param key + # The header to set. + # @param value + # The value to set for this header. + def default_header(key, value) + @default_headers[key] = value + end + + # @method request + # Perform a HTTP request. + # @param type + # The HTTP request type. + # @param path + # The path to perform the request against. + # @param data + # The data to send with the request. + # @param headers + # Hash with headers to add to the request. + # @param retry_count + # The amount of times to retry on exception. + # @param backoff + # Number of seconds to wait between each retry. + # @return Net::HTTPResponse + # A response object. + # @raises Puppet::ResourceError + # If invalid type parameter. + # @raises Puppet::ResourceError + # If request failed and retries are done. + def request(type, + path, + data=nil, + headers=nil, + query=nil, + retry_count=10, + backoff=5) + url = parse_url(path, query) + + Puppet.debug("#{type.upcase} request against #{url}") + + case type.upcase + when 'GET' + request = Net::HTTP::Get.new(url.request_uri) + when 'POST' + request = Net::HTTP::Post.new(url.request_uri) + when 'PUT' + request = Net::HTTP::Put.new(url.request_uri) + when 'PATCH' + request = Net::HTTP::Patch.new(url.request_uri) + when 'DELETE' + request = Net::HTTP::Delete.new(url.request_uri) + else + raise Puppet::ResourceError, "request called with unsupported HTTP request type: #{type}" + end + + request.content_type = 'application/json' + request.body = data if data + + all_headers = default_headers + all_headers = all_headers.merge!(headers) if headers && headers.is_a?(Hash) + + Puppet.debug("Headers that is set: #{all_headers.keys}") + + all_headers.each do |key, value| + if not value.is_a?(String) + val = value.to_s + else + val = value + end + request[key] = val + end + + # Always override the user-agent + request['User-Agent'] = 'puppet-openstack-client/1.0.0' + + retry_counter = retry_count + + begin + use_ssl = (url.scheme == 'https') + + if use_ssl + if @ca_file + Puppet.debug("Using CA file #{@ca_file} when doing request") + else + Puppet.warning('HTTPS request without any CA file, request cannot be verified!') + end + end + + response = Net::HTTP.start(url.hostname, url.port, + :use_ssl => use_ssl, + :ca_file => @ca_file) do |http| + http.request(request) + end + rescue => e + if retry_counter > 0 + Puppet.debug("Failed #{type.upcase} request to #{url} retrying #{retry_counter} more times: #{e.message}") + retry_counter -= 1 + + if backoff > 0 + Puppet.debug("Backing off for #{backoff} seconds until next try against #{url}") + Kernel.sleep backoff + end + + retry + end + + raise Puppet::ResourceError, "#{type.upcase} HTTP request to #{url.to_s} failed: #{e.message}" + end + + response + end + end +end diff --git a/lib/puppet_x/openstack/resource.rb b/lib/puppet_x/openstack/resource.rb new file mode 100644 index 00000000..95e871b5 --- /dev/null +++ b/lib/puppet_x/openstack/resource.rb @@ -0,0 +1,267 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet' +require 'puppet_x' +require 'json' + +DEFAULT_FILTER_KEYS = ['ensure'] + +module PuppetX::OpenStack + class Resource + # The resource data. + @resource_data = {} + + # Can be used to add more keys that will be filtered out. + @filter_keys = nil + + # Can be used to transform keys while keeping the value. + @transform_keys = nil + + # Can be used to override values everywhere, no matter what. + @overrides = nil + + # @method initialize + # Initialize the resource. + # @param resource_data + # Resource data to initialize with or nil + # to do it later. + # @param merge + # Boolean if we should merge or set the data. + def initialize(resource_data=nil, merge=true) + @resource_data = {} + + return if not resource_data + + initialize_resource_data(resource_data, merge) + end + + # @method [] + # Get a key from the resource data. + # @param key + # The key to get. + # @return value or nil + # The value of the key or nil + def [](key) + get(key) + end + + # @method get + # Get a key from the resource data. + # @param key + # The key to get. + # @return value or nil + # The value of the key or nil + def get(key) + if @resource_data.key?(key) + @resource_data[key] + else + nil + end + rescue + nil + end + + # @method []= + # Set a value in the resource data. + # @param key + # The key to set. + # @param value + # The value to set. + def []=(key, value) + set(key, value) + end + + # @method set + # Set a value in the resource data. + # @param key + # The key to set. + # @param value + # The value to set. + def set(key, value) + @resource_data[key] = value + end + + # @method key? + # Check if a key exists in the resource data. + # @param key + # The key to check. + # @return boolean + # True if the key exists otherwise false. + def key?(key) + @resource_data.key?(key) + end + + # @method delete + # Delete a key from the resource data. + # @param key + # The key to delete. + def delete(key) + @resource_data.delete(key) if key?(key) + end + + # @method from_type + # Initialize resource data from Puppet type data, + # filter out and transform keys. + # @param resource_data + # The resource data. + # @param merge + # Boolean if we should merge or set the data. + def from_type(resource_data, merge=true) + resource_data = filter_resource_data(resource_data) + resource_data = transform_resource_data(resource_data) + resource_data = override_resource_data(resource_data) + + initialize_resource_data(resource_data, merge) + end + + # @method to_json + # Generate JSON from the saved resource data. + # @param parent + # String to enclose the data inside a a parent key. + # @return string + # String with the generated JSON data. + def to_json(parent=nil) + if not @resource_data + raise Puppet::ResourceError, "Resource has no data cannot generate JSON" + end + + if parent + generate_data = { parent.to_s => @resource_data } + else + @resource_data + end + + JSON.generate(generate_data) + rescue + @resource_data + end + + # @method to_hash + # Get the resource data as a hash. + # @return hash + # Hash with all the resource data. + def to_hash + if not @resource_data + raise Puppet::ResourceError, "Resource has no data cannot return hash" + end + + result = @resource_data + result = transform_resource_data(result, true) + result = override_resource_data(result) + + result.transform_keys { |key| key.to_sym rescue key } + end + + # @method to_type + # Get the resource data and format to type. + # @param context + # The Puppet::ResourceApi::BaseContext context. + # @return hash + # Hash with all the resource data. + def to_type(context) + h = to_hash + h[:ensure] = 'present' + rejected_keys = context.type.check_schema_keys(h) + h.reject { |k, v| rejected_keys.include?(k) } + end + + private + + # @method initialize_resource_data + # Initialize the resource data. + # @param resource_data + # The resource data. + # @param merge + # Boolean if we should merge the data otherwise set it. + def initialize_resource_data(resource_data, merge) + if merge + @resource_data.merge!(resource_data) + else + @resource_data = resource_data + end + + @resource_data.each do |key, val| + define_singleton_method("#{key}=") { |new| @resource_data[key] = new } + define_singleton_method(key) { @resource_data[key] } + end + end + + # @method filter_resource_data + # Filter resource data. + # @param resource_data + # The resource data. + # @return hash + # The filtered hash with the resource data. + def filter_resource_data(resource_data) + filters = DEFAULT_FILTER_KEYS + filters += @filter_keys if @filter_keys + + Puppet.debug("Filtering out keys: #{filters.inspect}") + + resource_data.reject { |k, v| + filters.include?(k.to_s) || filters.include?(k.to_sym) + } + end + + # @method transform_resource_data + # Transform resource data. + # @param reverse + # Boolean if we should do a reverse transform. + # @return hash + # The transformed hash with the resource data. + def transform_resource_data(resource_data, reverse=false) + return resource_data if not @transform_keys + + if reverse + Puppet.debug("Reverse transforming keys: #{@transform_keys.values.inspect}") + else + Puppet.debug("Transforming keys: #{@transform_keys.keys.inspect}") + end + + @transform_keys.map do |left, right| + if reverse + from = right + to = left + else + from = left + to = right + end + + resource_data[to.to_sym] = resource_data.delete(from.to_sym) if resource_data.key?(from.to_sym) + resource_data[to.to_sym] = resource_data.delete(from.to_s) if resource_data.key?(from.to_s) + end + + resource_data + end + + # @method override_resource_data + # Process overrides. + # @param resource_data + # The resource data. + # @return hash + # The processed hash after overrides. + def override_resource_data(resource_data) + return resource_data if not @overrides + + @overrides.map do |key, value| + resource_data[key.to_sym] = value + end + + resource_data + end + end +end diff --git a/lib/puppet_x/openstack/token_request.rb b/lib/puppet_x/openstack/token_request.rb new file mode 100644 index 00000000..a229e84e --- /dev/null +++ b/lib/puppet_x/openstack/token_request.rb @@ -0,0 +1,101 @@ +# Copyright (c) 2019 Binero AB +# +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'puppet_x' + +module PuppetX::OpenStack + # Class whose purpose is to process credentials and generate + # a valid token authentication request body that can be sent + # to keystone. + class TokenRequest + @creds = nil + + # @method initialize + # Initialize the class. + def initialize(credentials) + @creds = credentials + end + + # @method body + # Create our hash that will be the body of the token request. + # @return hash + # A hash that contains all authentication data that ca be + # converted to JSON and sent to keystone. + def body + user_hash = @creds.user + user_hash[:password] = @creds.password + user_hash[:domain] = @creds.user_domain + + password_hash = { + :user => user_hash, + } + + methods = ['password'] + + identity_hash = { + :methods => methods, + :password => password_hash, + } + + auth_hash = { + :identity => identity_hash, + } + + body_hash = { + :auth => auth_hash, + } + + scope_hash = scope + body_hash[:auth][:scope] = scope_hash if scope_hash + + body_hash + end + + private + + # @method scope + # Get the hash with the proper scope data + # that will be added to the body. + # @return hash + # Nil or a hash with all data request for the specified scope. + def scope + case @creds.scope + when 'unscoped' + return nil + when 'system' + scope_data = { + :system => { + :all => true, + } + } + when 'domain' + scope_data = { + :domain => @creds.domain, + } + when 'project' + scope_data = { + :project => @creds.project, + } + + # When authenticating as a project we need + # to specify the project domain as well. + scope_data[:project][:domain] = @creds.project_domain + end + + scope_data + end + end +end