Add new providers
Adds the new base code for talking directly to OpenStack APIs for the provider resources. Change-Id: I4c989ddec2d3f14e91d7f4596bf9a0baf809fc57
This commit is contained in:
parent
3d397fc10c
commit
ad0718dfd2
|
@ -0,0 +1,95 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,169 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,232 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,206 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,267 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,101 @@
|
|||
# Copyright (c) 2019 Binero AB
|
||||
#
|
||||
# Author: Tobias Urdin <tobias.urdin@binero.se>
|
||||
#
|
||||
# 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
|
Loading…
Reference in New Issue