Blob Blame History Raw
From 1259caba4cdfcf96a2e47b81f8e3671ebf470329 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20M=C3=A1gr?= <mmagr@redhat.com>
Date: Fri, 3 Oct 2014 20:03:50 +0200
Subject: [PATCH] [heat] Implement Keystone domain creation

Keystone domain has to be created for Heat. This patch implements this
via helper script [1] since we don't have support for Keystone v3 API
in puppet-keystone yet. This implementation should be refactored as soon
as we will have v3 API available in puppet-keystone. For more info
please check [2].

[1] https://github.com/openstack/heat/blob/master/bin/heat-keystone-setup-domain
[2] https://bugzilla.redhat.com/show_bug.cgi?id=1076172
---
 .../puppet/provider/heat_domain_id_setter/ruby.rb  | 183 +++++++++++++++++++++
 heat/lib/puppet/type/heat_config.rb                |   4 +
 heat/lib/puppet/type/heat_domain_id_setter.rb      |  31 ++++
 heat/manifests/keystone/domain.pp                  |  73 ++++++++
 4 files changed, 291 insertions(+)
 create mode 100644 heat/lib/puppet/provider/heat_domain_id_setter/ruby.rb
 create mode 100644 heat/lib/puppet/type/heat_domain_id_setter.rb
 create mode 100644 heat/manifests/keystone/domain.pp

diff --git a/heat/lib/puppet/provider/heat_domain_id_setter/ruby.rb b/heat/lib/puppet/provider/heat_domain_id_setter/ruby.rb
new file mode 100644
index 0000000..8862fe5
--- /dev/null
+++ b/heat/lib/puppet/provider/heat_domain_id_setter/ruby.rb
@@ -0,0 +1,183 @@
+## NB: This must work with Ruby 1.8!
+
+# This provider permits the stack_user_domain parameter in heat.conf
+# to be set by providing a domain_name to the Puppet module and
+# using the Keystone REST API to translate the name into the corresponding
+# UUID.
+#
+# This requires that tenant names be unique.  If there are multiple matches
+# for a given tenant name, this provider will raise an exception.
+
+require 'rubygems'
+require 'net/http'
+require 'json'
+
+class KeystoneError < Puppet::Error
+end
+
+class KeystoneConnectionError < KeystoneError
+end
+
+class KeystoneAPIError < KeystoneError
+end
+
+# Provides common request handling semantics to the other methods in
+# this module.
+#
+# +req+::
+#   An HTTPRequest object
+# +url+::
+#   A parsed URL (returned from URI.parse)
+def handle_request(req, url)
+    begin
+        res = Net::HTTP.start(url.host, url.port) {|http|
+            http.request(req)
+        }
+
+        if res.code != '200'
+            raise KeystoneAPIError, "Received error response from Keystone server at #{url}: #{res.message}"
+        end
+    rescue Errno::ECONNREFUSED => detail
+        raise KeystoneConnectionError, "Failed to connect to Keystone server at #{url}: #{detail}"
+    rescue SocketError => detail
+        raise KeystoneConnectionError, "Failed to connect to Keystone server at #{url}: #{detail}"
+    end
+
+    res
+end
+
+# Authenticates to a Keystone server and obtains an authentication token.
+# It returns a 2-element +[token, authinfo]+, where +token+ is a token
+# suitable for passing to openstack apis in the +X-Auth-Token+ header, and
+# +authinfo+ is the complete response from Keystone, including the service
+# catalog (if available).
+#
+# +auth_url+::
+#   Keystone endpoint URL.  This function assumes API version
+#   2.0 and an administrative endpoint, so this will typically look like
+#   +http://somehost:35357/v2.0+.
+#
+# +username+::
+#   Username for authentication.
+#
+# +password+::
+#   Password for authentication
+#
+# +tenantID+::
+#   Tenant UUID
+#
+# +tenantName+::
+#   Tenant name
+#
+def keystone_v2_authenticate(auth_url,
+                             username,
+                             password,
+                             tenantId=nil,
+                             tenantName=nil)
+
+    post_args = {
+        'auth' => {
+            'passwordCredentials' => {
+                'username' => username,
+                'password' => password
+            },
+        }}
+
+    if tenantId
+        post_args['auth']['tenantId'] = tenantId
+    end
+
+    if tenantName
+        post_args['auth']['tenantName'] = tenantName
+    end
+
+    url = URI.parse("#{auth_url}/tokens")
+    req = Net::HTTP::Post.new url.path
+    req['content-type'] = 'application/json'
+    req.body = post_args.to_json
+
+    res = handle_request(req, url)
+    data = JSON.parse res.body
+    return data['access']['token']['id'], data
+end
+
+# Queries a Keystone server to a list of all tenants.
+#
+# +auth_url+::
+#   Keystone endpoint.  See the notes for +auth_url+ in
+#   +keystone_v2_authenticate+.
+#
+# +token+::
+#   A Keystone token that will be passed in requests as the value of the
+#   +X-Auth-Token+ header.
+#
+def keystone_v3_domains(auth_url,
+                        token)
+
+    auth_url.sub!('v2.0', 'v3')
+    url = URI.parse("#{auth_url}/domains")
+    req = Net::HTTP::Get.new url.path
+    req['content-type'] = 'application/json'
+    req['x-auth-token'] = token
+
+    res = handle_request(req, url)
+    data = JSON.parse res.body
+    data['domains']
+end
+
+Puppet::Type.type(:heat_domain_id_setter).provide(:ruby) do
+    def authenticate
+        token, authinfo = keystone_v2_authenticate(
+            @resource[:auth_url],
+            @resource[:auth_username],
+            @resource[:auth_password],
+            nil,
+            @resource[:auth_tenant_name])
+
+        return token
+    end
+
+    def find_domain_by_name(token)
+        domains = keystone_v3_domains(
+            @resource[:auth_url],
+            token)
+        domains.select{|domain| domain['name'] == @resource[:domain_name]}
+    end
+
+    def exists?
+        false
+    end
+
+    def create
+        config
+    end
+
+    # This looks for the domain specified by the 'domain_name' parameter to
+    # the resource and returns the corresponding UUID if there is a single
+    # match.
+    #
+    # Raises a KeystoneAPIError if:
+    #
+    # - There are multiple matches, or
+    # - There are zero matches
+    def get_domain_id
+        token = authenticate
+        domains = find_domain_by_name(token)
+
+        if domains.length == 1
+            return domains[0]['id']
+        elsif domains.length > 1
+            name = domains[0]['name']
+            raise KeystoneAPIError, 'Found multiple matches for domain name "#{name}"'
+        else
+            raise KeystoneAPIError, 'Unable to find matching tenant'
+        end
+    end
+
+    def config
+        Puppet::Type.type(:heat_config).new(
+            {:name => 'DEFAULT/stack_user_domain', :value => "#{get_domain_id}"}
+        ).create
+    end
+
+end
diff --git a/heat/lib/puppet/type/heat_config.rb b/heat/lib/puppet/type/heat_config.rb
index adcc5ce..75613a9 100644
--- a/heat/lib/puppet/type/heat_config.rb
+++ b/heat/lib/puppet/type/heat_config.rb
@@ -40,4 +40,8 @@ Puppet::Type.newtype(:heat_config) do
     defaultto false
   end
 
+  def create
+    provider.create
+  end
+
 end
diff --git a/heat/lib/puppet/type/heat_domain_id_setter.rb b/heat/lib/puppet/type/heat_domain_id_setter.rb
new file mode 100644
index 0000000..d6e1eee
--- /dev/null
+++ b/heat/lib/puppet/type/heat_domain_id_setter.rb
@@ -0,0 +1,31 @@
+Puppet::Type.newtype(:heat_domain_id_setter) do
+
+    ensurable
+
+    newparam(:name, :namevar => true) do
+        desc 'The name of the setting to update'
+    end
+
+    newparam(:domain_name) do
+        desc 'The heat domain name'
+    end
+
+    newparam(:auth_url) do
+        desc 'The Keystone endpoint URL'
+        defaultto 'http://localhost:35357/v2.0'
+    end
+
+    newparam(:auth_username) do
+        desc 'Username with which to authenticate'
+        defaultto 'admin'
+    end
+
+    newparam(:auth_password) do
+        desc 'Password with which to authenticate'
+    end
+
+    newparam(:auth_tenant_name) do
+        desc 'Tenant name with which to authenticate'
+        defaultto 'admin'
+    end
+end
diff --git a/heat/manifests/keystone/domain.pp b/heat/manifests/keystone/domain.pp
new file mode 100644
index 0000000..5cb98b7
--- /dev/null
+++ b/heat/manifests/keystone/domain.pp
@@ -0,0 +1,73 @@
+# == Class: heat::keystone::domain
+#
+# Configures heat domain in Keystone.
+#
+# Note: Implementation is done by heat-keystone-setup-domain script temporarily
+#       because currently puppet-keystone does not support v3 API
+#
+# === Parameters
+#
+# [*auth_url*]
+#   Keystone auth url
+#
+# [*keystone_admin*]
+#   Keystone admin user
+#
+# [*keystone_password*]
+#   Keystone admin password
+#
+# [*keystone_tenant*]
+#   Keystone admin tenant name
+#
+# [*domain_name*]
+#   Heat domain name. Defaults to 'heat'.
+#
+# [*domain_admin*]
+#   Keystone domain admin user which will be created. Defaults to 'heat_admin'.
+#
+# [*domain_password*]
+#   Keystone domain admin user password. Defaults to 'changeme'.
+#
+class heat::keystone::domain (
+  $auth_url          = undef,
+  $keystone_admin    = undef,
+  $keystone_password = undef,
+  $keystone_tenant   = undef,
+  $domain_name       = 'heat',
+  $domain_admin      = 'heat_admin',
+  $domain_password   = 'changeme',
+) {
+
+  include heat::params
+
+  $cmd_evn = [
+    "OS_USERNAME=${keystone_admin}",
+    "OS_PASSWORD=${keystone_password}",
+    "OS_AUTH_URL=${auth_url}",
+    "HEAT_DOMAIN=${domain_name}",
+    "HEAT_DOMAIN_ADMIN=${domain_admin}",
+    "HEAT_DOMAIN_PASSWORD=${domain_password}"
+  ]
+  exec { 'heat_domain_create':
+    path        => '/usr/bin',
+    command     => 'heat-keystone-setup-domain &>/dev/null',
+    environment => $cmd_evn,
+    require     => Package['heat-common'],
+  }
+
+  heat_domain_id_setter { 'heat_domain_id':
+    ensure           => present,
+    domain_name      => $domain_name,
+    auth_url         => $auth_url,
+    auth_username    => $keystone_admin,
+    auth_password    => $keystone_password,
+    auth_tenant_name => $keystone_tenant,
+    require          => Exec['heat_domain_create'],
+  }
+
+  heat_config {
+    'DEFAULT/stack_domain_admin': value => $domain_admin;
+    'DEFAULT/stack_domain_admin_password': value => $domain_password;
+  }
+
+}