Update to Kilo
authorMartin Zobel-Helas <zobel@debian.org>
Wed, 19 Aug 2015 13:25:51 +0000 (13:25 +0000)
committerMartin Zobel-Helas <zobel@debian.org>
Wed, 19 Aug 2015 13:25:51 +0000 (13:25 +0000)
Signed-off-by: Martin Zobel-Helas <zobel@debian.org>
42 files changed:
3rdparty/Puppetfile
3rdparty/modules/keystone/CHANGELOG.md
3rdparty/modules/keystone/README.md
3rdparty/modules/keystone/checksums.json [new file with mode: 0644]
3rdparty/modules/keystone/examples/v3_basic.pp [new file with mode: 0644]
3rdparty/modules/keystone/ext/keystone_test_v3.rb [new file with mode: 0644]
3rdparty/modules/keystone/files/httpd/keystone.py
3rdparty/modules/keystone/lib/puppet/provider/keystone.rb
3rdparty/modules/keystone/lib/puppet/provider/keystone/util.rb [new file with mode: 0644]
3rdparty/modules/keystone/lib/puppet/provider/keystone_domain/openstack.rb [new file with mode: 0644]
3rdparty/modules/keystone/lib/puppet/provider/keystone_role/openstack.rb
3rdparty/modules/keystone/lib/puppet/provider/keystone_service/openstack.rb
3rdparty/modules/keystone/lib/puppet/provider/keystone_tenant/openstack.rb
3rdparty/modules/keystone/lib/puppet/provider/keystone_user/openstack.rb
3rdparty/modules/keystone/lib/puppet/provider/keystone_user_role/openstack.rb
3rdparty/modules/keystone/lib/puppet/type/keystone_domain.rb [new file with mode: 0644]
3rdparty/modules/keystone/lib/puppet/type/keystone_tenant.rb
3rdparty/modules/keystone/lib/puppet/type/keystone_user.rb
3rdparty/modules/keystone/lib/puppet/type/keystone_user_role.rb
3rdparty/modules/keystone/manifests/endpoint.pp
3rdparty/modules/keystone/manifests/init.pp
3rdparty/modules/keystone/manifests/resource/authtoken.pp [new file with mode: 0644]
3rdparty/modules/keystone/manifests/resource/service_identity.pp
3rdparty/modules/keystone/manifests/roles/admin.pp
3rdparty/modules/keystone/metadata.json
3rdparty/modules/keystone/spec/acceptance/basic_keystone_spec.rb
3rdparty/modules/keystone/spec/classes/keystone_endpoint_spec.rb
3rdparty/modules/keystone/spec/classes/keystone_roles_admin_spec.rb
3rdparty/modules/keystone/spec/classes/keystone_spec.rb
3rdparty/modules/keystone/spec/defines/keystone_resource_authtoken_spec.rb [new file with mode: 0644]
3rdparty/modules/keystone/spec/defines/keystone_resource_service_identity_spec.rb
3rdparty/modules/keystone/spec/spec_helper_acceptance.rb
3rdparty/modules/keystone/spec/unit/provider/keystone/util_spec.rb [new file with mode: 0644]
3rdparty/modules/keystone/spec/unit/provider/keystone_domain/openstack_spec.rb [new file with mode: 0644]
3rdparty/modules/keystone/spec/unit/provider/keystone_role/openstack_spec.rb
3rdparty/modules/keystone/spec/unit/provider/keystone_service/openstack_spec.rb
3rdparty/modules/keystone/spec/unit/provider/keystone_spec.rb
3rdparty/modules/keystone/spec/unit/provider/keystone_tenant/openstack_spec.rb
3rdparty/modules/keystone/spec/unit/provider/keystone_user/openstack_spec.rb
3rdparty/modules/keystone/spec/unit/provider/keystone_user_role/openstack_spec.rb
3rdparty/modules/keystone/spec/unit/type/keystone_tenant_spec.rb [new file with mode: 0644]
3rdparty/modules/keystone/spec/unit/type/keystone_user_spec.rb [new file with mode: 0644]

index 29f9da5..722a92e 100644 (file)
@@ -12,7 +12,7 @@ mod 'elasticsearch/elasticsearch', '0.9.5'
 mod 'nanliu/staging', '1.0.3'
 
 # OpenStack
-mod 'stackforge/keystone', '5.1.0'
+mod 'openstack/keystone', '6.0.0'
 mod 'puppetlabs/apache', '1.5.0'
 mod 'stackforge/openstacklib', '5.1.0'
 mod 'aimonb/aviator', '0.5.1'
index 973da8f..33c0f5e 100644 (file)
@@ -1,3 +1,43 @@
+##2015-07-08 - 6.0.0
+###Summary
+
+This is a backwards-incompatible major release for OpenStack Kilo.
+
+####Backwards-incompatible changes
+- Remove deprecated parameters
+- MySQL: change default MySQL collate to utf8_general_ci
+- Move openstackclient to openstacklib
+
+####Features
+- Puppet 4.x support
+- Support Keystone v3 API
+- Allow disabling or delaying the token_flush cron
+- Migrate postgresql backend to use openstacklib::db::postgresql
+- Add max_token_size optional parameter
+- Add admin_workers and public_workers configuration options
+- Add support for LDAP connection pools
+- Add a package ensure for openstackclient
+- Enable setting the revoke/token driver
+- Add manage_service feature
+- Makes distinct use of url vs auth_url
+- Create a sync_db boolean for Keystone
+- LDAP: add support to configure credential driver
+- Support notification_format
+- Allow custom file source for wsgi scripts
+- Decouple sync_db from enabled
+- Add support for Fernet Tokens
+
+####Bugfixes
+- Crontab: ensure the script is run with bash shell
+- Copy latest keystone.py from Keystone upstream
+- Fix deprecated LDAP config options
+- Fix service keystone conflict when running in apache
+
+####Maintenance
+- Acceptance tests with Beaker
+- Fix spec tests for RSpec 3.x and Puppet 4.x
+- Restructures authentication for resource providers
+
 ##2015-06-17 - 5.1.0
 ###Summary
 
index df60237..e35380c 100644 (file)
@@ -1,7 +1,7 @@
 keystone
 =======
 
-5.1.0 - 2014.2 - Juno
+6.0.0 - 2015.1 - Kilo
 
 #### Table of Contents
 
diff --git a/3rdparty/modules/keystone/checksums.json b/3rdparty/modules/keystone/checksums.json
new file mode 100644 (file)
index 0000000..a39bc59
--- /dev/null
@@ -0,0 +1,94 @@
+{
+  "CHANGELOG.md": "88b3a502ab7375f624d0c0e01a8adaac",
+  "Gemfile": "2aec9171226d42dc7e5cf1f6bfc977bf",
+  "LICENSE": "12a15a9ebddda7d856c783f745e5ee47",
+  "README.md": "4d9691f4e7f088cad9baa3ef6737b32e",
+  "Rakefile": "68e2a46cd546eeb34bab6dc1512b549d",
+  "examples/apache_dropin.pp": "219828fe274f52e57b69b4b32f24b423",
+  "examples/apache_with_paths.pp": "b2122f0f4eefe9eedec2cade44d4dcc3",
+  "examples/ldap_full.pp": "943f823c8a45cacbbe65f0d8711d6287",
+  "examples/ldap_identity.pp": "ee258356235ece8a0416e747f3d1fd80",
+  "examples/v3_basic.pp": "f1cae4f18d02993b61ab5a63c1a06ed0",
+  "ext/keystone_test.rb": "d403c8c80616f94d0cac9ff12c327b9a",
+  "ext/keystone_test_v3.rb": "e6121a38bfe902ca8d3c00669b41af4e",
+  "files/httpd/keystone.py": "775825c125975916ef6bad1ef9faf44c",
+  "lib/puppet/provider/keystone/util.rb": "f1618166fbc1df3e160e795d5734428d",
+  "lib/puppet/provider/keystone.rb": "b1aa3281c13dcb476f0d4241d18609f2",
+  "lib/puppet/provider/keystone_config/ini_setting.rb": "b3c3813be1c155f49fedf0a1178fe905",
+  "lib/puppet/provider/keystone_domain/openstack.rb": "8e69b3404d2dc8b5a9a55ba13b7b40bd",
+  "lib/puppet/provider/keystone_endpoint/openstack.rb": "d69201cbcef5da1b588d356416841f75",
+  "lib/puppet/provider/keystone_paste_ini/ini_setting.rb": "df7e671676104f090a07942f774844e3",
+  "lib/puppet/provider/keystone_role/openstack.rb": "71d44ad4123acd8cb8a756e19e6d5017",
+  "lib/puppet/provider/keystone_service/openstack.rb": "f30c932bb9ab74fa6591302033e8f5f9",
+  "lib/puppet/provider/keystone_tenant/openstack.rb": "37407bf69e6097d89565b12257707cb4",
+  "lib/puppet/provider/keystone_user/openstack.rb": "d79c17b8c9f5ad3ae99e14f953ba958b",
+  "lib/puppet/provider/keystone_user_role/openstack.rb": "d6dddb86df2d8e3a9acde3f97d2bc60b",
+  "lib/puppet/type/keystone_config.rb": "01069a89da581af00fed130fc373c2c3",
+  "lib/puppet/type/keystone_domain.rb": "3c2791fdc77585edc0d56b25a9bd6b72",
+  "lib/puppet/type/keystone_endpoint.rb": "0e72c165df04b2efc73e1676871e0785",
+  "lib/puppet/type/keystone_paste_ini.rb": "5429630bad1a33ab3f14b45f403ed2af",
+  "lib/puppet/type/keystone_role.rb": "421ee2247a50fb2eacac5cdd1bcc7382",
+  "lib/puppet/type/keystone_service.rb": "09f5241ca8fede306018b36454deedd3",
+  "lib/puppet/type/keystone_tenant.rb": "9bdf0763ba4d610ad262437751cde59e",
+  "lib/puppet/type/keystone_user.rb": "2ed3068a9bdd3a19168a6cb09ff09bf3",
+  "lib/puppet/type/keystone_user_role.rb": "82f5d6d1c1fba9023adfaf11104bffe9",
+  "manifests/client.pp": "8520e2209d84d0ba92b5e6a429938731",
+  "manifests/config.pp": "5e27a3b503cd4931e410a2d41d89fda1",
+  "manifests/cron/token_flush.pp": "4f2ce0209fbb9696eda2758ef84c18d2",
+  "manifests/db/mysql.pp": "d91fa7b9a3ecf574af6a9f11d1d2c147",
+  "manifests/db/postgresql.pp": "caacd8b3028d50c37c61be6626d92673",
+  "manifests/db/sync.pp": "3caf7ccd37b6f62714bf3b77d0dbf0f9",
+  "manifests/dev/install.pp": "54e58af21326dcb21603096500e63be1",
+  "manifests/endpoint.pp": "d24b24be405b7b1e5fb1af76ecfa6500",
+  "manifests/init.pp": "e1c57737e028155032a6a4c06264a864",
+  "manifests/ldap.pp": "5c8b5b4937a59cc835ddb3609c3ba97e",
+  "manifests/logging.pp": "5774990dea77d17dfceaad4a8777824c",
+  "manifests/params.pp": "0b8b7920a55a5ac26af221ee9cd162c5",
+  "manifests/policy.pp": "c8a8998316ea42f611a1c7ae6a563461",
+  "manifests/python.pp": "88ef5e5584df349640335a1227f924d7",
+  "manifests/resource/authtoken.pp": "120f71ab6e1a6f27bf44a6c92cd5ab24",
+  "manifests/resource/service_identity.pp": "d0e6c682727ce2109f42300a35b0988e",
+  "manifests/roles/admin.pp": "b74dd7fa2a292f6e37ea3018c064c7e2",
+  "manifests/service.pp": "d043301073416c442d0a0fba36c85c0d",
+  "manifests/wsgi/apache.pp": "54606fca35ca48104799da62bd1799e4",
+  "metadata.json": "2aa859fa6bc99b9503bd05a01f5dd233",
+  "spec/acceptance/basic_keystone_spec.rb": "bb23467e03bd97fee508fa7f3a975174",
+  "spec/acceptance/nodesets/default.yml": "2bb6248df2563b1d871aeb2ab16e077a",
+  "spec/acceptance/nodesets/nodepool-centos7.yml": "cc37eb9368fb2bd889b3f6830cabcfc7",
+  "spec/acceptance/nodesets/nodepool-trusty.yml": "f68590f582842cdd73233946829644a3",
+  "spec/classes/keystone_client_spec.rb": "7f97bacdc6b372b160e1234ec576968a",
+  "spec/classes/keystone_cron_token_flush_spec.rb": "3aefefa8e13597338b329d0be9121c77",
+  "spec/classes/keystone_db_mysql_spec.rb": "025306de49f8f83823eff98321f18172",
+  "spec/classes/keystone_db_postgresql_spec.rb": "53fb2d31e72446e51feb6da576656d19",
+  "spec/classes/keystone_endpoint_spec.rb": "f9f73dd21b83b2334d4d1dee701c511f",
+  "spec/classes/keystone_ldap_spec.rb": "9c9cd5d034294c27f3739cd89509c471",
+  "spec/classes/keystone_logging_spec.rb": "662d3c86e9c2f5225a2ad1ef3721327d",
+  "spec/classes/keystone_policy_spec.rb": "179d795895cfabe3c3d02615b9e9a04a",
+  "spec/classes/keystone_python_spec.rb": "59d13966a2f62c0884f2d6375c0c0135",
+  "spec/classes/keystone_roles_admin_spec.rb": "1ff0a367ba5a130727e55c6aad985533",
+  "spec/classes/keystone_service_spec.rb": "15c4446865070015904ea9550eec2fa5",
+  "spec/classes/keystone_spec.rb": "0b73d50f264bbb6da25c6df078eb8407",
+  "spec/classes/keystone_wsgi_apache_spec.rb": "d0f9bf67abe16ece501758079a17a081",
+  "spec/defines/keystone_resource_authtoken_spec.rb": "f83a5fa982d4e4435e4c84bcc50bf52e",
+  "spec/defines/keystone_resource_service_identity_spec.rb": "216982cb0eb16ed223429440bc52bbfe",
+  "spec/shared_examples.rb": "c23e32dbc37052ae071bc729abfeeda5",
+  "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c",
+  "spec/spec_helper.rb": "ab7c5df754690421cdfe9770c3057cc8",
+  "spec/spec_helper_acceptance.rb": "a2737eefbf7340478b048aeb19de4cae",
+  "spec/unit/provider/keystone/util_spec.rb": "a1f871eef2314861a5e61769068eb0bb",
+  "spec/unit/provider/keystone_domain/openstack_spec.rb": "ad941a88ec119a6ed65e0751f74e978a",
+  "spec/unit/provider/keystone_endpoint/openstack_spec.rb": "1b54246797f8a9d245c90d4b2a6bc069",
+  "spec/unit/provider/keystone_paste_ini/ini_setting_spec.rb": "8b583280cfc7c67d64a2dfd8caa7a130",
+  "spec/unit/provider/keystone_role/openstack_spec.rb": "ea02769d8d105648a28ae829e19ecaeb",
+  "spec/unit/provider/keystone_service/openstack_spec.rb": "072e985f1f4b90524af9a73ee99e435e",
+  "spec/unit/provider/keystone_spec.rb": "0ff6ddcf326413510cd4a015cdaf64ea",
+  "spec/unit/provider/keystone_tenant/openstack_spec.rb": "62b15ac89ad356a0498a9f341e3531d4",
+  "spec/unit/provider/keystone_user/openstack_spec.rb": "fc442a5e039f1318dbe3c63df39c1f4d",
+  "spec/unit/provider/keystone_user_role/openstack_spec.rb": "708bdfa669e6db8f6b90d158dd6bb4da",
+  "spec/unit/type/keystone_endpoint_spec.rb": "5dbd0b540a452bae36218b2a8794a41e",
+  "spec/unit/type/keystone_paste_ini_spec.rb": "9037113f96850d5567e9dc7c540915ae",
+  "spec/unit/type/keystone_tenant_spec.rb": "b9c9319e924537191b596a03c5c1443d",
+  "spec/unit/type/keystone_user_role_spec.rb": "a03cfa9f55028d6a7f2a351582c1a93d",
+  "spec/unit/type/keystone_user_spec.rb": "d7e2fc65663ecc487f2f046a668dc505",
+  "tests/site.pp": "f0f28af600251f61f3f1299dd0749d24"
+}
\ No newline at end of file
diff --git a/3rdparty/modules/keystone/examples/v3_basic.pp b/3rdparty/modules/keystone/examples/v3_basic.pp
new file mode 100644 (file)
index 0000000..5777d2b
--- /dev/null
@@ -0,0 +1,47 @@
+# Example using v3 domains.  The admin user is created in the domain
+# named 'admin_domain', and assigned the role 'admin' in the 'admin'
+# project in the domain 'admin_domain'.  The keystone service account is
+# created in default domain, and assigned the
+# role 'admin' in the project 'services' in the default domain.
+# NOTE: Until all of the other services support using Keystone v3
+# with keystone_authtoken middleware that supports v3, they cannot
+# specify a domain for authentication, and so have to be in the
+# default domain.
+#
+# To be sure everything is working, run:
+#   $ export OS_IDENTITY_API_VERSION=3
+#   $ export OS_USERNAME=admin
+#   $ export OS_USER_DOMAIN_NAME=admin_domain
+#   $ export OS_PASSWORD=ChangeMe
+#   $ export OS_PROJECT_NAME=admin
+#   $ export OS_PROJECT_DOMAIN_NAME=admin_domain
+#   $ export OS_AUTH_URL=http://keystone.local:35357/v3
+#   $ openstack user list
+#
+
+Exec { logoutput => 'on_failure' }
+
+
+class { '::mysql::server': }
+class { '::keystone::db::mysql':
+  password => 'keystone',
+}
+class { '::keystone':
+  verbose             => true,
+  debug               => true,
+  database_connection => 'mysql://keystone:keystone@127.0.0.1/keystone',
+  admin_token         => 'admin_token',
+  enabled             => true,
+}
+class { '::keystone::roles::admin':
+  email               => 'test@example.tld',
+  password            => 'a_big_secret',
+  admin               => 'admin', # username
+  admin_tenant        => 'admin', # project name
+  admin_user_domain   => 'admin', # domain for user
+  admin_tenant_domain => 'admin', # domain for project
+}
+class { '::keystone::endpoint':
+  public_url => 'http://127.0.0.1:5000/',
+  admin_url  => 'http://127.0.0.1:35357/',
+}
diff --git a/3rdparty/modules/keystone/ext/keystone_test_v3.rb b/3rdparty/modules/keystone/ext/keystone_test_v3.rb
new file mode 100644 (file)
index 0000000..0d7550e
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env ruby
+# this script verifies that keystone has
+# been successfully installed using the instructions
+# found here: http://keystone.openstack.org/configuration.html
+# and can use the v3 api http://developer.openstack.org/api-ref-identity-v3.html
+
+begin
+  require 'rubygems'
+rescue
+  puts 'Could not require rubygems. This assumes puppet is not installed as a gem'
+end
+require 'open3'
+require 'fileutils'
+require 'puppet'
+require 'pp'
+
+username='admin'
+password='a_big_secret'
+# required to get a real services catalog
+project='openstack'
+user_domain='admin'
+project_domain='admin'
+
+# shared secret
+service_token='admin_token'
+
+def run_command(cmd)
+  Open3.popen3(cmd) do |stdin, stdout, stderr|
+    begin
+      stdout = stdout.read
+      puts "Response from token request:#{stdout}"
+      return stdout
+    rescue Exception => e
+      puts "Request failed, this sh*t is borked :( : details: #{e}"
+      exit 1
+    end
+  end
+end
+
+puts `puppet apply -e "package {curl: ensure => present }"`
+get_token = %(curl -D - -d '{"auth":{"identity":{"methods":["password"],"password":{"user":{"domain":{"name":"#{user_domain}"},"name":"#{username}","password": "#{password}"}}},"scope":{"project":{"domain":{"name":"#{project_domain}"},"name": "#{project}"}}}}' -H "Content-type: application/json" http://localhost:35357/v3/auth/tokens)
+token = nil
+
+puts "Running auth command: #{get_token}"
+rawoutput = run_command(get_token)
+if rawoutput =~ /X-Subject-Token: ([\w]+)/
+  token = $1
+else
+  puts "No token in output! #{rawoutput}"
+  exit 1
+end
+
+if token
+  puts "We were able to retrieve a token"
+  puts token
+  verify_token = "curl -H 'X-Auth-Token: #{service_token}' 'X-Subject-Token: #{token}' http://localhost:35357/v3/auth/tokens"
+  puts 'verifying token'
+  run_command(verify_token)
+  ['endpoints', 'projects', 'users'].each do |x|
+    puts "getting #{x}"
+    get_keystone_data = "curl -H 'X-Auth-Token: #{token}' http://localhost:35357/v3/#{x}"
+    pp PSON.load(run_command(get_keystone_data))
+  end
+end
index 81c5348..a893d26 100644 (file)
@@ -13,7 +13,8 @@
 #    under the License.
 
 #
-# This file was copied from https://github.com/openstack/keystone/raw/a4f29db2b8cde1b445b86218fb5543295da2092c/httpd/keystone.py
+# This file was copied from
+# raw.githubusercontent.com/openstack/keystone/a4f29db/httpd/keystone.py
 # It's only required for platforms on which it is not packaged yet.
 # It should be removed when available everywhere in a package.
 #
index 4e7815a..849868b 100644 (file)
@@ -2,6 +2,7 @@ require 'puppet/util/inifile'
 require 'puppet/provider/openstack'
 require 'puppet/provider/openstack/auth'
 require 'puppet/provider/openstack/credentials'
+require 'puppet/provider/keystone/util'
 
 class Puppet::Provider::Keystone < Puppet::Provider::Openstack
 
@@ -30,6 +31,31 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
     @admin_endpoint ||= get_admin_endpoint
   end
 
+  # use the domain in this order:
+  # 1 - the domain name specified in the resource definition - resource[:domain]
+  # 2 - the domain name part of the resource name/title e.g. user_name::user_domain
+  #     if passed in by name_and_domain above
+  # 3 - use the specified default_domain_name
+  # 4 - lookup the default domain
+  # 5 - use 'Default' - the "default" default domain if no other one is configured
+  # Usage: name_and_domain(resource[:name], resource[:domain], default_domain_name)
+  def self.name_and_domain(namedomstr, domain_from_resource=nil, default_domain_name=nil)
+    name, domain = Util.split_domain(namedomstr)
+    ret = [name]
+    if domain_from_resource
+      ret << domain_from_resource
+    elsif domain
+      ret << domain
+    elsif default_domain_name
+      ret << default_domain_name
+    elsif default_domain
+      ret << default_domain
+    else
+      ret << 'Default'
+    end
+    ret
+  end
+
   def self.admin_token
     @admin_token ||= get_admin_token
   end
@@ -80,8 +106,8 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
 
   def self.request(service, action, properties=nil)
     super
-    rescue Puppet::Error::OpenstackAuthInputError => error
-      request_by_service_token(service, action, error, properties)
+  rescue Puppet::Error::OpenstackAuthInputError => error
+    request_by_service_token(service, action, error, properties)
   end
 
   def self.request_by_service_token(service, action, error, properties=nil)
@@ -96,6 +122,31 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
     INI_FILENAME
   end
 
+  def self.default_domain
+    domain_hash[default_domain_id]
+  end
+
+  def self.domain_hash
+    return @domain_hash if @domain_hash
+    list = request('domain', 'list')
+    @domain_hash = Hash[list.collect{|domain| [domain[:id], domain[:name]]}]
+    @domain_hash
+  end
+
+  def self.domain_name_from_id(id)
+    domain_hash[id]
+  end
+
+  def self.default_domain_id
+    return @default_domain_id if @default_domain_id
+    if keystone_file and keystone_file['identity'] and keystone_file['identity']['default_domain_id']
+      @default_domain_id = "#{keystone_file['identity']['default_domain_id'].strip}"
+    else
+      @default_domain_id = 'default'
+    end
+    @default_domain_id
+  end
+
   def self.keystone_file
     return @keystone_file if @keystone_file
     if File.exists?(ini_filename)
diff --git a/3rdparty/modules/keystone/lib/puppet/provider/keystone/util.rb b/3rdparty/modules/keystone/lib/puppet/provider/keystone/util.rb
new file mode 100644 (file)
index 0000000..7eadf98
--- /dev/null
@@ -0,0 +1,25 @@
+module Util
+  # Splits the rightmost part of a string using '::' as delimiter
+  # Returns an array of both parts or nil if either is empty.
+  # An empty rightmost part is ignored and converted as 'string::' => 'string'
+  #
+  # Examples:
+  # "foo"             -> ["foo", nil]
+  # "foo::"           -> ["foo", nil]
+  # "foo::bar"        -> ["foo", "bar"]
+  # "foo::bar::"      -> ["foo", "bar"]
+  # "::foo"           -> [nil, "foo"]
+  # "::foo::"         -> [nil, "foo"]
+  # "foo::bar::baz"   -> ["foo::bar", "baz"]
+  # "foo::bar::baz::" -> ["foo::bar", "baz"]
+  #
+  def self.split_domain(str)
+    left, right = nil, nil
+    unless str.nil?
+      left, delimiter, right = str.gsub(/::$/, '').rpartition('::')
+      left, right = right, nil if delimiter.empty?
+      left = nil if left.empty?
+    end
+    return [left, right]
+  end
+end
diff --git a/3rdparty/modules/keystone/lib/puppet/provider/keystone_domain/openstack.rb b/3rdparty/modules/keystone/lib/puppet/provider/keystone_domain/openstack.rb
new file mode 100644 (file)
index 0000000..14a8f69
--- /dev/null
@@ -0,0 +1,143 @@
+require 'puppet/provider/keystone'
+require 'puppet/util/inifile'
+
+Puppet::Type.type(:keystone_domain).provide(
+  :openstack,
+  :parent => Puppet::Provider::Keystone
+) do
+
+  desc 'Provider that manages keystone domains'
+
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
+
+  def initialize(value={})
+    super(value)
+    @property_flush = {}
+  end
+
+  def create
+    properties = [resource[:name]]
+    if resource[:enabled] == :true
+      properties << '--enable'
+    elsif resource[:enabled] == :false
+      properties << '--disable'
+    end
+    if resource[:description]
+      properties << '--description'
+      properties << resource[:description]
+    end
+    @property_hash = self.class.request('domain', 'create', properties)
+    @property_hash[:is_default] = sym_to_bool(resource[:is_default])
+    @property_hash[:ensure] = :present
+    ensure_default_domain(true)
+  end
+
+  def exists?
+    @property_hash[:ensure] == :present
+  end
+
+  def destroy
+    # have to disable first - Keystone does not allow you to delete an
+    # enabled domain
+    self.class.request('domain', 'set', [resource[:name], '--disable'])
+    self.class.request('domain', 'delete', resource[:name])
+    @property_hash[:ensure] == :absent
+    ensure_default_domain(false, true)
+    @property_hash.clear
+  end
+
+  def enabled=(value)
+    @property_flush[:enabled] = value
+  end
+
+  def enabled
+    bool_to_sym(@property_hash[:enabled])
+  end
+
+  def description=(value)
+    @property_flush[:description] = value
+  end
+
+  def description
+    @property_hash[:description]
+  end
+
+  def id
+    @property_hash[:id]
+  end
+
+  def is_default
+    bool_to_sym(@property_hash[:is_default])
+  end
+
+  def is_default=(value)
+    @property_flush[:is_default] = value
+  end
+
+  def ensure_default_domain(create, destroy=false, value=nil)
+    if !self.class.keystone_file
+      return
+    end
+    changed = false
+    curid = self.class.default_domain_id
+    newid = id
+    default = (is_default == :true)
+    if (default && create) || (!default && (value == :true))
+      # new default domain, or making existing domain the default domain
+      if curid != newid
+        self.class.keystone_file['identity']['default_domain_id'] = newid
+        changed = true
+      end
+    elsif (default && destroy) || (default && (value == :false))
+      # removing default domain, or making this domain not the default
+      if curid == newid
+        # can't delete from inifile, so just reset to default 'default'
+        self.class.keystone_file['identity']['default_domain_id'] = 'default'
+        changed = true
+        newid = 'default'
+      end
+    end
+    if changed
+      self.class.keystone_file.store
+      debug("The default_domain_id was changed from #{curid} to #{newid}")
+    end
+  end
+
+  def self.instances
+    request('domain', 'list').collect do |domain|
+      new(
+        :name        => domain[:name],
+        :ensure      => :present,
+        :enabled     => domain[:enabled].downcase.chomp == 'true' ? true : false,
+        :description => domain[:description],
+        :id          => domain[:id],
+        :is_default  => domain[:id] == default_domain_id
+      )
+    end
+  end
+
+  def self.prefetch(resources)
+    domains = instances
+    resources.keys.each do |name|
+      if provider = domains.find{ |domain| domain.name == name }
+        resources[name].provider = provider
+      end
+    end
+  end
+
+  def flush
+    options = []
+    if @property_flush && !@property_flush.empty?
+      options << '--enable' if @property_flush[:enabled] == :true
+      options << '--disable' if @property_flush[:enabled] == :false
+      if @property_flush[:description]
+        options << '--description' << resource[:description]
+      end
+      self.class.request('domain', 'set', [resource[:name]] + options) unless options.empty?
+      if @property_flush[:is_default]
+        ensure_default_domain(false, false, @property_flush[:is_default])
+      end
+      @property_flush.clear
+    end
+  end
+end
index b154620..799bce0 100644 (file)
@@ -7,7 +7,7 @@ Puppet::Type.type(:keystone_role).provide(
 
   desc 'Provider for keystone roles.'
 
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def initialize(value={})
     super(value)
index 40aa8e2..4ac7646 100644 (file)
@@ -7,7 +7,7 @@ Puppet::Type.type(:keystone_service).provide(
 
   desc "Provider to manage keystone services."
 
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def initialize(value={})
     super(value)
@@ -15,21 +15,17 @@ Puppet::Type.type(:keystone_service).provide(
   end
 
   def create
-    properties = []
-    if resource[:description]
-      properties << '--description'
-      properties << resource[:description]
+    if resource[:type]
+      properties = [resource[:type]]
+      properties << '--name' << resource[:name]
+      if resource[:description]
+        properties << '--description' << resource[:description]
+      end
+      self.class.request('service', 'create', properties)
+      @property_hash[:ensure] = :present
+    else
+      raise(Puppet::Error, 'The type is mandatory for creating a keystone service')
     end
-    raise(Puppet::Error, 'The service type is mandatory') unless resource[:type]
-    properties << '--type'
-    properties << resource[:type]
-    properties << resource[:name]
-    self.class.request('service', 'create', properties)
-    @property_hash[:ensure] = :present
-  end
-
-  def exists?
-    @property_hash[:ensure] == :present
   end
 
   def destroy
@@ -37,22 +33,26 @@ Puppet::Type.type(:keystone_service).provide(
     @property_hash.clear
   end
 
-  def description=(value)
-    @property_flush[:description] = value
+  def exists?
+    @property_hash[:ensure] == :present
   end
 
   def description
     @property_hash[:description]
   end
 
-  def type=(value)
-    @property_flush[:type] = value
+  def description=(value)
+    @property_flush[:description] = value
   end
 
   def type
     @property_hash[:type]
   end
 
+  def type=(value)
+    @property_flush[:type] = value
+  end
+
   def id
     @property_hash[:id]
   end
@@ -80,9 +80,11 @@ Puppet::Type.type(:keystone_service).provide(
   end
 
   def flush
-    if ! @property_flush.empty?
-      destroy
-      create
+    options = []
+    if @property_flush && !@property_flush.empty?
+      options << "--description=#{resource[:description]}" if @property_flush[:description]
+      options << "--type=#{resource[:type]}" if @property_flush[:type]
+      self.class.request('service', 'set', [@property_hash[:id]] + options) unless options.empty?
       @property_flush.clear
     end
   end
index 1987984..57a299f 100644 (file)
@@ -7,7 +7,7 @@ Puppet::Type.type(:keystone_tenant).provide(
 
   desc "Provider to manage keystone tenants/projects."
 
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def initialize(value={})
     super(value)
@@ -15,7 +15,9 @@ Puppet::Type.type(:keystone_tenant).provide(
   end
 
   def create
-    properties = [resource[:name]]
+    # see if resource[:domain], or project_name::project_domain
+    project_name, project_domain = self.class.name_and_domain(resource[:name], resource[:domain])
+    properties = [project_name]
     if resource[:enabled] == :true
       properties << '--enable'
     elsif resource[:enabled] == :false
@@ -25,8 +27,12 @@ Puppet::Type.type(:keystone_tenant).provide(
       properties << '--description'
       properties << resource[:description]
     end
-     self.class.request('project', 'create', properties)
-     @property_hash[:ensure] = :present
+    if project_domain
+      properties << '--domain'
+      properties << project_domain
+    end
+    @property_hash = self.class.request('project', 'create', properties)
+    @property_hash[:ensure] = :present
   end
 
   def exists?
@@ -34,7 +40,7 @@ Puppet::Type.type(:keystone_tenant).provide(
   end
 
   def destroy
-    self.class.request('project', 'delete', @property_hash[:id])
+    self.class.request('project', 'delete', id)
     @property_hash.clear
   end
 
@@ -54,29 +60,67 @@ Puppet::Type.type(:keystone_tenant).provide(
     @property_hash[:description]
   end
 
+  def domain
+    @property_hash[:domain]
+  end
+
   def id
     @property_hash[:id]
   end
 
   def self.instances
+    instance_hash = {}
     list = request('project', 'list', '--long')
-    list.collect do |project|
+    list.each do |project|
+      domname = domain_name_from_id(project[:domain_id])
+      if instance_hash.include?(project[:name]) # not unique
+        curdomid = instance_hash[project[:name]][:domain_id]
+        if curdomid != default_domain_id
+          # Move the project from the short name slot to the long name slot
+          # because it is not in the default domain.
+          curdomname = domain_name_from_id(curdomid)
+          instance_hash["#{project[:name]}::#{curdomname}"] = instance_hash[project[:name]]
+          # Use the short name slot for the new project
+          instance_hash[project[:name]] = project
+        else
+          # Use the long name for the new project
+          instance_hash["#{project[:name]}::#{domname}"] = project
+        end
+      else
+        # Unique (for now) - store in short name slot
+        instance_hash[project[:name]] = project
+      end
+    end
+    instance_hash.keys.collect do |project_name|
+      project = instance_hash[project_name]
+      domname = domain_name_from_id(project[:domain_id])
       new(
-        :name        => project[:name],
+        :name        => project_name,
         :ensure      => :present,
         :enabled     => project[:enabled].downcase.chomp == 'true' ? true : false,
         :description => project[:description],
+        :domain      => domname,
+        :domain_id   => project[:domain_id],
         :id          => project[:id]
       )
     end
   end
 
   def self.prefetch(resources)
-    tenants = instances
-    resources.keys.each do |name|
-       if provider = tenants.find{ |tenant| tenant.name == name }
-        resources[name].provider = provider
+    project_hash = {}
+    projects = instances
+    resources.each do |resname, resource|
+      # resname may be specified as just "name" or "name::domain"
+      name, resdomain = name_and_domain(resname, resource[:domain])
+      provider = projects.find do |project|
+        # have a match if the full instance name matches the full resource name, OR
+        # the base resource name matches the base instance name, and the
+        # resource domain matches the instance domain
+        project_name, project_domain = name_and_domain(project.name, project.domain)
+        (project.name == resname) ||
+          ((project_name == name) && (project_domain == resdomain))
       end
+      resource.provider = provider if provider
     end
   end
 
@@ -90,8 +134,7 @@ Puppet::Type.type(:keystone_tenant).provide(
         options << '--disable'
       end
       (options << "--description=#{resource[:description]}") if @property_flush[:description]
-      options << @property_hash[:id]
-      self.class.request('project', 'set', options) unless options.empty?
+      self.class.request('project', 'set', [id] + options) unless options.empty?
       @property_flush.clear
     end
   end
index 98a34cd..eb1e303 100644 (file)
@@ -7,7 +7,7 @@ Puppet::Type.type(:keystone_user).provide(
 
   desc "Provider to manage keystone users."
 
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def initialize(value={})
     super(value)
@@ -15,7 +15,9 @@ Puppet::Type.type(:keystone_user).provide(
   end
 
   def create
-    properties = [resource[:name]]
+    # see if resource[:domain], or user specified as user::domain
+    user_name, user_domain = self.class.name_and_domain(resource[:name], resource[:domain])
+    properties = [user_name]
     if resource[:enabled] == :true
       properties << '--enable'
     elsif resource[:enabled] == :false
@@ -24,18 +26,26 @@ Puppet::Type.type(:keystone_user).provide(
     if resource[:password]
       properties << '--password' << resource[:password]
     end
-    if resource[:tenant]
-      properties << '--project' << resource[:tenant]
-    end
     if resource[:email]
       properties << '--email' << resource[:email]
     end
-    self.class.request('user', 'create', properties)
+    if user_domain
+      properties << '--domain'
+      properties << user_domain
+    end
+    @property_hash = self.class.request('user', 'create', properties)
+    @property_hash[:domain] = user_domain
+    if resource[:tenant]
+      # DEPRECATED - To be removed in next release (Liberty)
+      # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+      project_id = Puppet::Resource.indirection.find("Keystone_tenant/#{resource[:tenant]}")[:id]
+      set_project(resource[:tenant], project_id)
+    end
     @property_hash[:ensure] = :present
   end
 
   def destroy
-    self.class.request('user', 'delete', @property_hash[:id])
+    self.class.request('user', 'delete', id)
     @property_hash.clear
   end
 
@@ -89,11 +99,23 @@ Puppet::Type.type(:keystone_user).provide(
       res = resource[:password]
     else
       # Password validation
-      credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
-      credentials.auth_url     = self.class.get_endpoint
-      credentials.password     = resource[:password]
-      credentials.project_name = resource[:tenant]
-      credentials.username     = resource[:name]
+      credentials                  = Puppet::Provider::Openstack::CredentialsV3.new
+      credentials.auth_url         = self.class.get_endpoint
+      credentials.password         = resource[:password]
+      credentials.user_id          = id
+      # NOTE: The only reason we use username is so that the openstack provider
+      # will know we are doing v3password auth - otherwise, it is not used.  The
+      # user_id uniquely identifies the user including domain.
+      credentials.username, unused = self.class.name_and_domain(resource[:name], domain)
+      # Need to specify a project id to get a project scoped token.  List
+      # all of the projects for the user, and use the id from the first one.
+      projects = self.class.request('project', 'list', ['--user', id, '--long'])
+      if projects && projects[0] && projects[0][:id]
+        credentials.project_id = projects[0][:id]
+      else
+        # last chance - try a domain scoped token
+        credentials.domain_name = domain
+      end
       begin
         token = Puppet::Provider::Openstack.request('token', 'issue', ['--format', 'value'], credentials)
       rescue Puppet::Error::OpenstackUnauthorizedError
@@ -117,6 +139,51 @@ Puppet::Type.type(:keystone_user).provide(
     @property_flush[:replace_password] = value
   end
 
+  def find_project_for_user(projname, project_id = nil)
+    # DEPRECATED - To be removed in next release (Liberty)
+    # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+    user_name, user_domain = self.class.name_and_domain(resource[:name], resource[:domain])
+    project_name, project_domain = self.class.name_and_domain(projname, nil, user_domain)
+    self.class.request('project', 'list', ['--user', id, '--long']).each do |project|
+      if (project_id == project[:id]) ||
+         ((projname == project_name) && (project_domain == self.class.domain_name_from_id(project[:domain_id])))
+        return project[:name]
+      end
+    end
+    return nil
+  end
+
+  def set_project(newproject, project_id = nil)
+    # DEPRECATED - To be removed in next release (Liberty)
+    # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+    unless project_id
+      project_id = Puppet::Resource.indirection.find("Keystone_tenant/#{newproject}")[:id]
+    end
+    # Currently the only way to assign a user to a tenant not using user-create
+    # is to use role-add - this means we also need a role - there is usual
+    # a default role called _member_ which can be used for this purpose.  What
+    # usually happens in a puppet module is that immediately after calling
+    # keystone_user, the module will then assign a role to that user.  It is
+    # ok for a user to have the _member_ role and another role.
+    default_role = "_member_"
+    begin
+      self.class.request('role', 'show', default_role)
+    rescue
+      self.class.request('role', 'create', default_role)
+    end
+    # finally, assign the user to the project with the role
+    self.class.request('role', 'add', [default_role, '--project', project_id, '--user', id])
+    newproject
+  end
+
+  # DEPRECATED - To be removed in next release (Liberty)
+  # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+  def tenant=(value)
+    @property_hash[:tenant] = set_project(value)
+  end
+
+  # DEPRECATED - To be removed in next release (Liberty)
+  # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
   def tenant
     return resource[:tenant] if sym_to_bool(resource[:ignore_default_tenant])
     # use the one returned from instances
@@ -130,40 +197,52 @@ Puppet::Type.type(:keystone_user).provide(
     if tenant_name.nil? or tenant_name.empty?
       return nil # nothing found, nothing given
     end
-    # If the user list command doesn't report the project, it might still be there
-    # We don't need to know exactly what it is, we just need to know whether it's
-    # the one we're trying to set.
-    roles = self.class.request('user role', 'list', [resource[:name], '--project', tenant_name])
-    if roles.empty?
-      return nil
-    else
-      return tenant_name
-    end
+    project_id = Puppet::Resource.indirection.find("Keystone_tenant/#{tenant_name}")[:id]
+    find_project_for_user(tenant_name, project_id)
   end
 
-  def tenant=(value)
-    self.class.request('user', 'set', [resource[:name], '--project', value])
-    rescue Puppet::ExecutionFailure => e
-      if e.message =~ /You are not authorized to perform the requested action: LDAP user update/
-        # read-only LDAP identity backend - just fall through
-      else
-        raise e
-      end
-      # note: read-write ldap will silently fail, not raise an exception
-    else
-    @property_hash[:tenant] = self.class.set_project(value, resource[:name])
+  def domain
+    @property_hash[:domain]
+  end
+
+  def domain_id
+    @property_hash[:domain_id]
   end
 
   def self.instances
-    list = request('user', 'list', '--long')
-    list.collect do |user|
+    instance_hash = {}
+    request('user', 'list', ['--long']).each do |user|
+      # The field says "domain" but it is really the domain_id
+      domname = domain_name_from_id(user[:domain])
+      if instance_hash.include?(user[:name]) # not unique
+        curdomid = instance_hash[user[:name]][:domain]
+        if curdomid != default_domain_id
+          # Move the user from the short name slot to the long name slot
+          # because it is not in the default domain.
+          curdomname = domain_name_from_id(curdomid)
+          instance_hash["#{user[:name]}::#{curdomname}"] = instance_hash[user[:name]]
+          # Use the short name slot for the new user
+          instance_hash[user[:name]] = user
+        else
+          # Use the long name for the new user
+          instance_hash["#{user[:name]}::#{domname}"] = user
+        end
+      else
+        # Unique (for now) - store in short name slot
+        instance_hash[user[:name]] = user
+      end
+    end
+    instance_hash.keys.collect do |user_name|
+      user = instance_hash[user_name]
       new(
-        :name        => user[:name],
+        :name        => user_name,
         :ensure      => :present,
         :enabled     => user[:enabled].downcase.chomp == 'true' ? true : false,
         :password    => user[:password],
-        :project     => user[:project],
         :email       => user[:email],
+        :description => user[:description],
+        :domain      => domain_name_from_id(user[:domain]),
+        :domain_id   => user[:domain],
         :id          => user[:id]
       )
     end
@@ -171,34 +250,19 @@ Puppet::Type.type(:keystone_user).provide(
 
   def self.prefetch(resources)
     users = instances
-    resources.keys.each do |name|
-       if provider = users.find{ |user| user.name == name }
-        resources[name].provider = provider
+    resources.each do |resname, resource|
+      # resname may be specified as just "name" or "name::domain"
+      name, resdomain = name_and_domain(resname, resource[:domain])
+      provider = users.find do |user|
+        # have a match if the full instance name matches the full resource name, OR
+        # the base resource name matches the base instance name, and the
+        # resource domain matches the instance domain
+        username, user_domain = name_and_domain(user.name, user.domain)
+        (user.name == resname) ||
+          ((username == name) && (user_domain == resdomain))
       end
+      resource.provider = provider if provider
     end
   end
 
-  def self.set_project(newproject, name)
-    # some backends do not store the project/tenant in the user object, so we have to
-    # to modify the project/tenant instead
-    # First, see if the project actually needs to change
-    roles = request('user role', 'list', [name, '--project', newproject])
-    unless roles.empty?
-      return # if already set, just skip
-    end
-    # Currently the only way to assign a user to a tenant not using user-create
-    # is to use user-role-add - this means we also need a role - there is usual
-    # a default role called _member_ which can be used for this purpose.  What
-    # usually happens in a puppet module is that immediately after calling
-    # keystone_user, the module will then assign a role to that user.  It is
-    # ok for a user to have the _member_ role and another role.
-    default_role = "_member_"
-    begin
-      request('role', 'show', [default_role])
-    rescue
-      debug("Keystone role #{default_role} does not exist - creating")
-      request('role', 'create', [default_role])
-    end
-    request('role', 'add', [default_role, '--project', newproject, '--user', name])
-  end
 end
index da2b870..e670a67 100644 (file)
@@ -1,4 +1,5 @@
 require 'puppet/provider/keystone'
+require 'puppet/provider/keystone/util'
 
 Puppet::Type.type(:keystone_user_role).provide(
   :openstack,
@@ -7,7 +8,7 @@ Puppet::Type.type(:keystone_user_role).provide(
 
   desc "Provider to manage keystone role assignments to users."
 
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def initialize(value={})
     super(value)
@@ -15,9 +16,6 @@ Puppet::Type.type(:keystone_user_role).provide(
   end
 
   def create
-    properties = []
-    properties << '--project' << get_project
-    properties << '--user' << get_user
     if resource[:roles]
       resource[:roles].each do |role|
         self.class.request('role', 'add', [role] + properties)
@@ -26,9 +24,6 @@ Puppet::Type.type(:keystone_user_role).provide(
   end
 
   def destroy
-    properties = []
-    properties << '--project' << get_project
-    properties << '--user' << get_user
     if @property_hash[:roles]
       @property_hash[:roles].each do |role|
         self.class.request('role', 'remove', [role] + properties)
@@ -38,10 +33,8 @@ Puppet::Type.type(:keystone_user_role).provide(
   end
 
   def exists?
-    if @user_role_hash
-      return ! @property_hash[:name].empty?
-    else
-      roles = self.class.request('user role', 'list', [get_user, '--project', get_project])
+    if self.class.user_role_hash.nil? || self.class.user_role_hash.empty?
+      roles = self.class.request('role', 'list', properties)
       # Since requesting every combination of users, roles, and
       # projects is so expensive, construct the property hash here
       # instead of in self.instances so it can be used in the role
@@ -55,8 +48,8 @@ Puppet::Type.type(:keystone_user_role).provide(
           role[:name]
         end
       end
-      return @property_hash[:ensure] == :present
     end
+    return @property_hash[:ensure] == :present
   end
 
   def roles
@@ -68,13 +61,11 @@ Puppet::Type.type(:keystone_user_role).provide(
     # determine the roles to be added and removed
     remove = current_roles - Array(value)
     add    = Array(value) - current_roles
-    user = get_user
-    project = get_project
     add.each do |role_name|
-      self.class.request('role', 'add', [role_name, '--project', project, '--user', user])
+      self.class.request('role', 'add', [role_name] + properties)
     end
     remove.each do |role_name|
-      self.class.request('role', 'remove', [role_name, '--project', project, '--user', user])
+      self.class.request('role', 'remove', [role_name] + properties)
     end
   end
 
@@ -91,6 +82,19 @@ Puppet::Type.type(:keystone_user_role).provide(
 
   private
 
+  def properties
+    properties = []
+    if get_project_id
+      properties << '--project' << get_project_id
+    elsif get_domain
+      properties << '--domain' << get_domain
+    else
+      error("No project or domain specified for role")
+    end
+    properties << '--user' << get_user_id
+    properties
+  end
+
   def get_user
     resource[:name].rpartition('@').first
   end
@@ -99,12 +103,67 @@ Puppet::Type.type(:keystone_user_role).provide(
     resource[:name].rpartition('@').last
   end
 
+  # if the role is for a domain, it will be specified as
+  # user@::domain - the "project" part will be empty
+  def get_domain
+    # use defined because @domain may be nil
+    return @domain if defined?(@domain)
+    projname, domname = Util.split_domain(get_project)
+    if projname.nil?
+      @domain = domname # no project specified, so must be a domain
+    else
+      @domain = nil # not a domain specific role
+    end
+    @domain
+  end
+
+  def get_user_id
+    @user_id ||= Puppet::Resource.indirection.find("Keystone_user/#{get_user}")[:id]
+  end
+
+  def get_project_id
+    # use defined because @project_id may be nil
+    return @project_id if defined?(@project_id)
+    projname, domname = Util.split_domain(get_project)
+    if projname.nil?
+      @project_id = nil
+    else
+      @project_id ||= Puppet::Resource.indirection.find("Keystone_tenant/#{get_project}")[:id]
+    end
+    @project_id
+  end
+
   def self.get_projects
-    request('project', 'list').collect { |project| project[:name] }
+    request('project', 'list', '--long').collect do |project|
+      {
+        :id        => project[:id],
+        :name      => project[:name],
+        :domain_id => project[:domain_id],
+        :domain    => domain_name_from_id(project[:domain_id])
+      }
+    end
   end
 
-  def self.get_users(project)
-    request('user', 'list', ['--project', project]).collect { |user| user[:name] }
+  def self.get_users(project_id=nil, domain_id=nil)
+    properties = ['--long']
+    if project_id
+      properties << '--project' << project_id
+    elsif domain_id
+      properties << '--domain' << domain_id
+    end
+    request('user', 'list', properties).collect do |user|
+      {
+        :id        => user[:id],
+        :name      => user[:name],
+        # note - column is "Domain" but it is really the domain id
+        :domain_id => user[:domain],
+        :domain    => domain_name_from_id(user[:domain])
+      }
+    end
+  end
+
+  def self.user_role_hash
+    @user_role_hash
   end
 
   def self.set_user_role_hash(user_role_hash)
@@ -112,16 +171,32 @@ Puppet::Type.type(:keystone_user_role).provide(
   end
 
   def self.build_user_role_hash
-    hash = @user_role_hash || {}
+    # The new hash will have the property that if the
+    # given key does not exist, create it with an empty
+    # array as the value for the hash key
+    hash = @user_role_hash || Hash.new{|h,k| h[k] = []}
     return hash unless hash.empty?
-    projects = get_projects
-    projects.each do |project|
-      users = get_users(project)
-      users.each do |user|
-        user_roles = request('user role', 'list', [user, '--project', project])
-        hash["#{user}@#{project}"] = []
-        user_roles.each do |role|
-          hash["#{user}@#{project}"] << role[:name]
+    # Need a mapping of project id to names.
+    project_hash = {}
+    Puppet::Type.type(:keystone_tenant).provider(:openstack).instances.each do |project|
+      project_hash[project.id] = project.name
+    end
+    # Need a mapping of user id to names.
+    user_hash = {}
+    Puppet::Type.type(:keystone_user).provider(:openstack).instances.each do |user|
+      user_hash[user.id] = user.name
+    end
+    # need a mapping of role id to name
+    role_hash = {}
+    request('role', 'list').each {|role| role_hash[role[:id]] = role[:name]}
+    # now, get all role assignments
+    request('role assignment', 'list').each do |assignment|
+      if assignment[:user]
+        if assignment[:project]
+          hash["#{user_hash[assignment[:user]]}@#{project_hash[assignment[:project]]}"] << role_hash[assignment[:role]]
+        else
+          domainname = domain_id_to_name(assignment[:domain])
+          hash["#{user_hash[assignment[:user]]}@::#{domainname}"] << role_hash[assignment[:role]]
         end
       end
     end
diff --git a/3rdparty/modules/keystone/lib/puppet/type/keystone_domain.rb b/3rdparty/modules/keystone/lib/puppet/type/keystone_domain.rb
new file mode 100644 (file)
index 0000000..4a2d777
--- /dev/null
@@ -0,0 +1,54 @@
+# LP#1408531
+File.expand_path('../..', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
+File.expand_path('../../../../openstacklib/lib', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
+
+Puppet::Type.newtype(:keystone_domain) do
+
+  desc <<-EOT
+    This type can be used to manage
+    keystone domains.
+  EOT
+
+  ensurable
+
+  newparam(:name, :namevar => true) do
+    newvalues(/\w+/)
+  end
+
+  newproperty(:enabled) do
+    newvalues(/(t|T)rue/, /(f|F)alse/, true, false )
+    defaultto(true)
+    munge do |value|
+      value.to_s.downcase.to_sym
+    end
+  end
+
+  newproperty(:description)
+
+  newproperty(:is_default) do
+    desc <<-EOT
+      If this is true, this is the default domain used for v2.0 requests when the domain
+      is not specified, or used by v3 providers if no other domain is specified.  The id
+      of this domain will be written to the keystone config identity/default_domain_id
+      value.
+    EOT
+    newvalues(/(t|T)rue/, /(f|F)alse/, true, false )
+    defaultto(false)
+    munge do |value|
+      value.to_s.downcase.to_sym
+    end
+  end
+
+  newproperty(:id) do
+    validate do |v|
+      raise(Puppet::Error, 'This is a read only property')
+    end
+  end
+
+  # we should not do anything until the keystone service is started
+  autorequire(:service) do
+    'keystone'
+  end
+
+
+end
index 6195d23..449ccd0 100644 (file)
@@ -1,6 +1,7 @@
 # LP#1408531
 File.expand_path('../..', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
 File.expand_path('../../../../openstacklib/lib', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
+require 'puppet/provider/keystone/util'
 
 Puppet::Type.newtype(:keystone_tenant) do
 
@@ -34,6 +35,20 @@ Puppet::Type.newtype(:keystone_tenant) do
     end
   end
 
+  newproperty(:domain) do
+    desc 'Domain for tenant.'
+    newvalues(nil, /\S+/)
+    def insync?(is)
+      raise(Puppet::Error, "The domain cannot be changed from #{self.should} to #{is}") unless self.should == is
+      true
+    end
+  end
+
+  autorequire(:keystone_domain) do
+    # use the domain parameter if given, or the one from name if any
+    self[:domain] || Util.split_domain(self[:name])[1]
+  end
+
   # This ensures the service is started and therefore the keystone
   # config is configured IF we need them for authentication.
   # If there is no keystone config, authentication credentials
index b484e7c..f671f3e 100644 (file)
@@ -2,6 +2,8 @@
 File.expand_path('../..', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
 File.expand_path('../../../../openstacklib/lib', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
 
+require 'puppet/provider/keystone/util'
+
 Puppet::Type.newtype(:keystone_user) do
 
   desc 'Type for managing keystone users.'
@@ -13,6 +15,11 @@ Puppet::Type.newtype(:keystone_user) do
   end
 
   newparam(:ignore_default_tenant) do
+    # DEPRECATED - To be removed in next release (Liberty)
+    # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+    validate do |v|
+      Puppet.warning('The ignore_default_tenant parameter is deprecated and will be removed in the future.')
+    end
     newvalues(/(t|T)rue/, /(f|F)alse/, true, false)
     defaultto(false)
     munge do |value|
@@ -48,6 +55,11 @@ Puppet::Type.newtype(:keystone_user) do
   end
 
   newproperty(:tenant) do
+    # DEPRECATED - To be removed in next release (Liberty)
+    # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
+    validate do |v|
+      Puppet.warning('The tenant parameter is deprecated and will be removed in the future. Please use keystone_user_role to assign a user to a project.')
+    end
     newvalues(/\S+/)
   end
 
@@ -69,10 +81,25 @@ Puppet::Type.newtype(:keystone_user) do
     end
   end
 
+  newproperty(:domain) do
+    newvalues(nil, /\S+/)
+    def insync?(is)
+      raise(Puppet::Error, "The domain cannot be changed from #{self.should} to #{is}") unless self.should == is
+      true
+    end
+  end
+
   autorequire(:keystone_tenant) do
+    # DEPRECATED - To be removed in next release (Liberty)
+    # https://bugs.launchpad.net/puppet-keystone/+bug/1472437
     self[:tenant]
   end
 
+  autorequire(:keystone_domain) do
+    # use the domain parameter if given, or the one from name if any
+    self[:domain] or Util.split_domain(self[:name])[1]
+  end
+
   # we should not do anything until the keystone service is started
   autorequire(:service) do
     ['keystone']
index 502dc39..d3c78e9 100644 (file)
@@ -2,6 +2,8 @@
 File.expand_path('../..', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
 File.expand_path('../../../../openstacklib/lib', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
 
+require 'puppet/provider/keystone/util'
+
 Puppet::Type.newtype(:keystone_user_role) do
 
   desc <<-EOT
@@ -31,13 +33,31 @@ Puppet::Type.newtype(:keystone_user_role) do
   end
 
   autorequire(:keystone_tenant) do
-    self[:name].rpartition('@').last
+    proj, dom = Util.split_domain(self[:name].rpartition('@').last)
+    rv = nil
+    if proj # i.e. not ::domain
+      rv = self[:name].rpartition('@').last
+    end
+    rv
   end
 
   autorequire(:keystone_role) do
     self[:roles]
   end
 
+  autorequire(:keystone_domain) do
+    rv = []
+    userdom = Util.split_domain(self[:name].rpartition('@').first)[1]
+    if userdom
+      rv << userdom
+    end
+    projectdom = Util.split_domain(self[:name].rpartition('@').last)[1]
+    if projectdom
+      rv << projectdom
+    end
+    rv
+  end
+
   # we should not do anything until the keystone service is started
   autorequire(:service) do
     ['keystone']
index 6c821f4..ddf5c13 100644 (file)
 # [*version*]
 #   (optional) API version for endpoint. Appended to all endpoint urls. (Defaults to 'v2.0')
 #
+# [*user_domain*]
+#   (Optional) Domain for $auth_name
+#   Defaults to undef (use the keystone server default domain)
+#
+# [*project_domain*]
+#   (Optional) Domain for $tenant (project)
+#   Defaults to undef (use the keystone server default domain)
+#
+# [*default_domain*]
+#   (Optional) Domain for $auth_name and $tenant (project)
+#   If keystone_user_domain is not specified, use $keystone_default_domain
+#   If keystone_project_domain is not specified, use $keystone_default_domain
+#   Defaults to undef
+#
 # === Examples
 #
 #  class { 'keystone::endpoint':
@@ -36,6 +50,9 @@ class keystone::endpoint (
   $admin_url         = 'http://127.0.0.1:35357',
   $version           = 'v2.0',
   $region            = 'RegionOne',
+  $user_domain       = undef,
+  $project_domain    = undef,
+  $default_domain    = undef,
 ) {
 
   $public_url_real = "${public_url}/${version}"
@@ -56,6 +73,9 @@ class keystone::endpoint (
     admin_url           => $admin_url_real,
     internal_url        => $internal_url_real,
     region              => $region,
+    user_domain         => $user_domain,
+    project_domain      => $project_domain,
+    default_domain      => $default_domain,
   }
 
 }
index 20c2011..b406e32 100644 (file)
 #   (Optional) Number of maximum active Fernet keys. Integer > 0.
 #   Defaults to undef
 #
+# [*default_domain*]
+#   (optional) When Keystone v3 support is enabled, v2 clients will need
+#   to have a domain assigned for certain operations.  For example,
+#   doing a user create operation must have a domain associated with it.
+#   This is the domain which will be used if a domain is needed and not
+#   explicitly set in the request.
+#   Defaults to undef (will use built-in Keystone default)
+#
 # == Dependencies
 #  None
 #
@@ -467,6 +475,7 @@ class keystone(
   $enable_fernet_setup    = false,
   $fernet_key_repository  = '/etc/keystone/fernet-keys',
   $fernet_max_active_keys = undef,
+  $default_domain         = undef,
   # DEPRECATED PARAMETERS
   $mysql_module           = undef,
   $compute_port           = undef,
@@ -927,4 +936,27 @@ class keystone(
     }
   }
 
+  if $default_domain {
+    keystone_domain { $default_domain:
+      ensure     => present,
+      enabled    => true,
+      is_default => true,
+      require    => File['/etc/keystone/keystone.conf'],
+      notify     => Exec['restart_keystone'],
+    }
+    # Update this code when https://bugs.launchpad.net/keystone/+bug/1472285 is addressed.
+    # 1/ Keystone needs to be started before creating the default domain
+    # 2/ Once the default domain is created, we can query Keystone to get the default domain ID
+    # 3/ The Keystone_domain provider has in charge of doing the query and configure keystone.conf
+    # 4/ After such a change, we need to restart Keystone service.
+    # restart_keystone exec is doing 4/, it restart Keystone if we have a new default domain setted
+    # and if we manage the service to be enabled.
+    if $manage_service and $enabled {
+      exec { 'restart_keystone':
+        path        => ['/usr/sbin', '/usr/bin', '/sbin', '/bin/'],
+        command     => "service ${service_name} restart",
+        refreshonly => true,
+      }
+    }
+  }
 }
diff --git a/3rdparty/modules/keystone/manifests/resource/authtoken.pp b/3rdparty/modules/keystone/manifests/resource/authtoken.pp
new file mode 100644 (file)
index 0000000..5165abb
--- /dev/null
@@ -0,0 +1,253 @@
+# == Definition: keystone::resource::authtoken
+#
+# This resource configures Keystone authentication resources for an OpenStack
+# service.  It will manage the [keystone_authtoken] section in the given
+# config resource.  It supports all of the authentication parameters specified
+# at http://www.jamielennox.net/blog/2015/02/17/loading-authentication-plugins/
+# with the addition of the default domain for user and project.
+#
+# The username and project_name parameters may be given in the form
+# "name::domainname".  The authtoken resource will use the domains in
+# the following order:
+# 1) The given domain parameter (user_domain_name or project_domain_name)
+# 2) The domain given as the "::domainname" part of username or project_name
+# 3) The default_domain_name
+#
+# For example, instead of doing this::
+#
+#     glance_api_config {
+#       'keystone_authtoken/admin_tenant_name': value => $keystone_tenant;
+#       'keystone_authtoken/admin_user'       : value => $keystone_user;
+#       'keystone_authtoken/admin_password'   : value => $keystone_password;
+#       secret => true;
+#       ...
+#     }
+#
+# manifests should do this instead::
+#
+#     keystone::resource::authtoken { 'glance_api_config':
+#       username            => $keystone_user,
+#       password            => $keystone_password,
+#       auth_url            => $real_identity_uri,
+#       project_name        => $keystone_tenant,
+#       user_domain_name    => $keystone_user_domain,
+#       project_domain_name => $keystone_project_domain,
+#       default_domain_name => $keystone_default_domain,
+#       cacert              => $ca_file,
+#       ...
+#     }
+#
+# The use of `keystone::resource::authtoken` makes it easy to avoid mistakes,
+# and makes it easier to support some of the newer authentication types coming
+# with Keystone Kilo and later, such as Kerberos, Federation, etc.
+#
+# == Parameters:
+#
+# [*name*]
+#   The name of the resource corresponding to the config file.  For example,
+#   keystone::resource::authtoken { 'glance_api_config': ... }
+#   Where 'glance_api_config' is the name of the resource used to manage
+#   the glance api configuration.
+#   string; required
+#
+# [*username*]
+#   The name of the service user;
+#   string; required
+#
+# [*password*]
+#   Password to create for the service user;
+#   string; required
+#
+# [*auth_url*]
+#   The URL to use for authentication.
+#   string; required
+#
+# [*auth_plugin*]
+#   The plugin to use for authentication.
+#   string; optional: default to 'password'
+#
+# [*user_id*]
+#   The ID of the service user;
+#   string; optional: default to undef
+#
+# [*user_domain_name*]
+#   (Optional) Name of domain for $username
+#   Defaults to undef
+#
+# [*user_domain_id*]
+#   (Optional) ID of domain for $username
+#   Defaults to undef
+#
+# [*project_name*]
+#   Service project name;
+#   string; optional: default to undef
+#
+# [*project_id*]
+#   Service project ID;
+#   string; optional: default to undef
+#
+# [*project_domain_name*]
+#   (Optional) Name of domain for $project_name
+#   Defaults to undef
+#
+# [*project_domain_id*]
+#   (Optional) ID of domain for $project_name
+#   Defaults to undef
+#
+# [*domain_name*]
+#   (Optional) Use this for auth to obtain a domain-scoped token.
+#   If using this option, do not specify $project_name or $project_id.
+#   Defaults to undef
+#
+# [*domain_id*]
+#   (Optional) Use this for auth to obtain a domain-scoped token.
+#   If using this option, do not specify $project_name or $project_id.
+#   Defaults to undef
+#
+# [*default_domain_name*]
+#   (Optional) Name of domain for $username and $project_name
+#   If user_domain_name is not specified, use $default_domain_name
+#   If project_domain_name is not specified, use $default_domain_name
+#   Defaults to undef
+#
+# [*default_domain_id*]
+#   (Optional) ID of domain for $user_id and $project_id
+#   If user_domain_id is not specified, use $default_domain_id
+#   If project_domain_id is not specified, use $default_domain_id
+#   Defaults to undef
+#
+# [*trust_id*]
+#   (Optional) Trust ID
+#   Defaults to undef
+#
+# [*cacert*]
+#   (Optional) CA certificate file for TLS (https)
+#   Defaults to undef
+#
+# [*cert*]
+#   (Optional) Certificate file for TLS (https)
+#   Defaults to undef
+#
+# [*key*]
+#   (Optional) Key file for TLS (https)
+#   Defaults to undef
+#
+# [*insecure*]
+#   If true, explicitly allow TLS without checking server cert against any
+#   certificate authorities.  WARNING: not recommended.  Use with caution.
+#   boolean; Defaults to false (which means be secure)
+#
+define keystone::resource::authtoken(
+  $username,
+  $password,
+  $auth_url,
+  $auth_plugin         = 'password',
+  $user_id             = undef,
+  $user_domain_name    = undef,
+  $user_domain_id      = undef,
+  $project_name        = undef,
+  $project_id          = undef,
+  $project_domain_name = undef,
+  $project_domain_id   = undef,
+  $domain_name         = undef,
+  $domain_id           = undef,
+  $default_domain_name = undef,
+  $default_domain_id   = undef,
+  $trust_id            = undef,
+  $cacert              = undef,
+  $cert                = undef,
+  $key                 = undef,
+  $insecure            = false,
+) {
+
+  if !$project_name and !$project_id and !$domain_name and !$domain_id {
+    fail('Must specify either a project (project_name or project_id, for a project scoped token) or a domain (domain_name or domain_id, for a domain scoped token)')
+  }
+
+  if ($project_name or $project_id) and ($domain_name or $domain_id) {
+    fail('Cannot specify both a project (project_name or project_id) and a domain (domain_name or domain_id)')
+  }
+
+  $user_and_domain_array = split($username, '::')
+  $real_username = $user_and_domain_array[0]
+  $real_user_domain_name = pick($user_domain_name, $user_and_domain_array[1], $default_domain_name, '__nodomain__')
+
+  $project_and_domain_array = split($project_name, '::')
+  $real_project_name = $project_and_domain_array[0]
+  $real_project_domain_name = pick($project_domain_name, $project_and_domain_array[1], $default_domain_name, '__nodomain__')
+
+  create_resources($name, {'keystone_authtoken/auth_plugin' => {'value' => $auth_plugin}})
+  create_resources($name, {'keystone_authtoken/auth_url' => {'value' => $auth_url}})
+  create_resources($name, {'keystone_authtoken/username' => {'value' => $real_username}})
+  create_resources($name, {'keystone_authtoken/password' => {'value' => $password, 'secret' => true}})
+  if $user_id {
+    create_resources($name, {'keystone_authtoken/user_id' => {'value' => $user_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/user_id' => {'ensure' => 'absent'}})
+  }
+  if $real_user_domain_name == '__nodomain__' {
+    create_resources($name, {'keystone_authtoken/user_domain_name' => {'ensure' => 'absent'}})
+  } else {
+    create_resources($name, {'keystone_authtoken/user_domain_name' => {'value' => $real_user_domain_name}})
+  }
+  if $user_domain_id {
+    create_resources($name, {'keystone_authtoken/user_domain_id' => {'value' => $user_domain_id}})
+  } elsif $default_domain_id {
+    create_resources($name, {'keystone_authtoken/user_domain_id' => {'value' => $default_domain_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/user_domain_id' => {'ensure' => 'absent'}})
+  }
+  if $project_name {
+    create_resources($name, {'keystone_authtoken/project_name' => {'value' => $real_project_name}})
+  } else {
+    create_resources($name, {'keystone_authtoken/project_name' => {'ensure' => 'absent'}})
+  }
+  if $project_id {
+    create_resources($name, {'keystone_authtoken/project_id' => {'value' => $project_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/project_id' => {'ensure' => 'absent'}})
+  }
+  if $real_project_domain_name == '__nodomain__' {
+    create_resources($name, {'keystone_authtoken/project_domain_name' => {'ensure' => 'absent'}})
+  } else {
+    create_resources($name, {'keystone_authtoken/project_domain_name' => {'value' => $real_project_domain_name}})
+  }
+  if $project_domain_id {
+    create_resources($name, {'keystone_authtoken/project_domain_id' => {'value' => $project_domain_id}})
+  } elsif $default_domain_id {
+    create_resources($name, {'keystone_authtoken/project_domain_id' => {'value' => $default_domain_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/project_domain_id' => {'ensure' => 'absent'}})
+  }
+  if $domain_name {
+    create_resources($name, {'keystone_authtoken/domain_name' => {'value' => $domain_name}})
+  } else {
+    create_resources($name, {'keystone_authtoken/domain_name' => {'ensure' => 'absent'}})
+  }
+  if $domain_id {
+    create_resources($name, {'keystone_authtoken/domain_id' => {'value' => $domain_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/domain_id' => {'ensure' => 'absent'}})
+  }
+  if $trust_id {
+    create_resources($name, {'keystone_authtoken/trust_id' => {'value' => $trust_id}})
+  } else {
+    create_resources($name, {'keystone_authtoken/trust_id' => {'ensure' => 'absent'}})
+  }
+  if $cacert {
+    create_resources($name, {'keystone_authtoken/cacert' => {'value' => $cacert}})
+  } else {
+    create_resources($name, {'keystone_authtoken/cacert' => {'ensure' => 'absent'}})
+  }
+  if $cert {
+    create_resources($name, {'keystone_authtoken/cert' => {'value' => $cert}})
+  } else {
+    create_resources($name, {'keystone_authtoken/cert' => {'ensure' => 'absent'}})
+  }
+  if $key {
+    create_resources($name, {'keystone_authtoken/key' => {'value' => $key}})
+  } else {
+    create_resources($name, {'keystone_authtoken/key' => {'ensure' => 'absent'}})
+  }
+  create_resources($name, {'keystone_authtoken/insecure' => {'value' => $insecure}})
+}
index 9bbd1b1..4ac1322 100644 (file)
 #   List of roles;
 #   string; optional: default to ['admin']
 #
-# [*domain*]
-#   User domain (keystone v3), not implemented yet.
-#   string; optional: default to undef
-#
 # [*email*]
 #   Service email;
 #   string; optional: default to '$auth_name@localhost'
 #   Whether to create the service.
 #   string; optional: default to True
 #
+# [*user_domain*]
+#   (Optional) Domain for $auth_name
+#   Defaults to undef (use the keystone server default domain)
+#
+# [*project_domain*]
+#   (Optional) Domain for $tenant (project)
+#   Defaults to undef (use the keystone server default domain)
+#
+# [*default_domain*]
+#   (Optional) Domain for $auth_name and $tenant (project)
+#   If keystone_user_domain is not specified, use $keystone_default_domain
+#   If keystone_project_domain is not specified, use $keystone_default_domain
+#   Defaults to undef
+#
 define keystone::resource::service_identity(
   $admin_url             = false,
   $internal_url          = false,
@@ -104,7 +114,6 @@ define keystone::resource::service_identity(
   $configure_user        = true,
   $configure_user_role   = true,
   $configure_service     = true,
-  $domain                = undef,
   $email                 = "${name}@localhost",
   $region                = 'RegionOne',
   $service_name          = undef,
@@ -112,19 +121,32 @@ define keystone::resource::service_identity(
   $tenant                = 'services',
   $ignore_default_tenant = false,
   $roles                 = ['admin'],
+  $user_domain           = undef,
+  $project_domain        = undef,
+  $default_domain        = undef,
 ) {
-
-  if $domain {
-    warning('Keystone domains are not yet managed by puppet-keystone.')
-  }
-
   if $service_name == undef {
     $service_name_real = $auth_name
   } else {
     $service_name_real = $service_name
   }
 
+  if $user_domain == undef {
+    $user_domain_real = $default_domain
+  } else {
+    $user_domain_real = $user_domain
+  }
+
   if $configure_user {
+    if $user_domain_real {
+      # We have to use ensure_resource here and hope for the best, because we have
+      # no way to know if the $user_domain is the same domain passed as the
+      # $default_domain parameter to class keystone.
+      ensure_resource('keystone_domain', $user_domain_real, {
+        'ensure'  => 'present',
+        'enabled' => true,
+      })
+    }
     ensure_resource('keystone_user', $auth_name, {
       'ensure'                => 'present',
       'enabled'               => true,
@@ -132,6 +154,7 @@ define keystone::resource::service_identity(
       'email'                 => $email,
       'tenant'                => $tenant,
       'ignore_default_tenant' => $ignore_default_tenant,
+      'domain'                => $user_domain_real,
     })
   }
 
@@ -140,9 +163,6 @@ define keystone::resource::service_identity(
       'ensure' => 'present',
       'roles'  => $roles,
     })
-    if $configure_user {
-      Keystone_user[$auth_name] -> Keystone_user_role["${auth_name}@${tenant}"]
-    }
   }
 
   if $configure_service {
index aa5abd7..fe43a0f 100644 (file)
 #
 # [*configure_user_role*]
 #   Optional. Should the admin role be configured for the admin user?
-#   Defaulst to 'true'.
+#   Defaults to 'true'.
+#
+# [*admin_user_domain*]
+#   Optional.  Domain of the admin user
+#   Defaults to undef (undef will resolve to class keystone $default_domain)
+#
+# [*admin_project_domain*]
+#   Optional.  Domain of the admin tenant
+#   Defaults to undef (undef will resolve to class keystone $default_domain)
+#
+# [*service_project_domain*]
+#   Optional.  Domain for $service_tenant
+#   Defaults to undef (undef will resolve to class keystone $default_domain)
 #
 # == Dependencies
 # == Examples
@@ -75,17 +87,55 @@ class keystone::roles::admin(
   $service_tenant_desc    = 'Tenant for the openstack services',
   $configure_user         = true,
   $configure_user_role    = true,
+  $admin_user_domain      = undef,
+  $admin_project_domain   = undef,
+  $service_project_domain = undef,
 ) {
 
+  if $service_project_domain {
+    if $service_project_domain != $admin_user_domain {
+      if $service_project_domain != $admin_project_domain {
+        keystone_domain { $service_project_domain:
+          ensure  => present,
+          enabled => true,
+        }
+      }
+    }
+  }
+
+  if $admin_project_domain {
+    if $admin_project_domain != $admin_user_domain {
+      if $service_project_domain != $admin_project_domain {
+        keystone_domain { $admin_project_domain:
+          ensure  => present,
+          enabled => true,
+        }
+      }
+    }
+  }
+
+  if $admin_user_domain {
+    if $admin_project_domain != $admin_user_domain {
+      if $service_project_domain != $admin_user_domain {
+        keystone_domain { $admin_user_domain:
+          ensure  => present,
+          enabled => true,
+        }
+      }
+    }
+  }
+
   keystone_tenant { $service_tenant:
     ensure      => present,
     enabled     => true,
     description => $service_tenant_desc,
+    domain      => $service_project_domain,
   }
   keystone_tenant { $admin_tenant:
     ensure      => present,
     enabled     => true,
     description => $admin_tenant_desc,
+    domain      => $admin_project_domain,
   }
   keystone_role { 'admin':
     ensure => present,
@@ -98,6 +148,7 @@ class keystone::roles::admin(
       tenant                => $admin_tenant,
       email                 => $email,
       password              => $password,
+      domain                => $admin_user_domain,
       ignore_default_tenant => $ignore_default_tenant,
     }
   }
index 77e23af..c7205b3 100644 (file)
@@ -1,39 +1,55 @@
 {
-  "name": "stackforge-keystone",
-  "version": "5.1.0",
+  "name": "openstack-keystone",
+  "version": "6.0.0",
   "author": "Puppet Labs and OpenStack Contributors",
   "summary": "Puppet module for OpenStack Keystone",
   "license": "Apache-2.0",
   "source": "git://github.com/openstack/puppet-keystone.git",
   "project_page": "https://launchpad.net/puppet-keystone",
   "issues_url": "https://bugs.launchpad.net/puppet-keystone",
+  "dependencies": [
+    {"name":"puppetlabs/apache","version_requirement":">=1.0.0 <2.0.0"},
+    {"name":"puppetlabs/inifile","version_requirement":">=1.0.0 <2.0.0"},
+    {"name":"puppetlabs/stdlib","version_requirement":">=4.0.0 <5.0.0"},
+    {"name":"openstack/openstacklib","version_requirement":">=6.0.0 <7.0.0"}
+  ],
   "requirements": [
-    { "name": "pe","version_requirement": "3.x" },
-    { "name": "puppet","version_requirement": "3.x" }
+    {
+      "name": "pe",
+      "version_requirement": "3.x"
+    },
+    {
+      "name": "puppet",
+      "version_requirement": "3.x"
+    }
   ],
   "operatingsystem_support": [
     {
       "operatingsystem": "Debian",
-      "operatingsystemrelease": ["7"]
+      "operatingsystemrelease": [
+        "7"
+      ]
     },
     {
       "operatingsystem": "Fedora",
-      "operatingsystemrelease": ["20"]
+      "operatingsystemrelease": [
+        "20"
+      ]
     },
     {
       "operatingsystem": "RedHat",
-      "operatingsystemrelease": ["6.5","7"]
+      "operatingsystemrelease": [
+        "6.5",
+        "7"
+      ]
     },
     {
       "operatingsystem": "Ubuntu",
-      "operatingsystemrelease": ["12.04","14.04"]
+      "operatingsystemrelease": [
+        "12.04",
+        "14.04"
+      ]
     }
   ],
-  "description": "Installs and configures OpenStack Keystone (Identity).",
-  "dependencies": [
-    { "name": "puppetlabs/apache", "version_requirement": ">=1.0.0 <2.0.0" },
-    { "name": "puppetlabs/inifile", "version_requirement": ">=1.0.0 <2.0.0" },
-    { "name": "puppetlabs/stdlib", "version_requirement": ">=4.0.0 <5.0.0" },
-    { "name": "stackforge/openstacklib", "version_requirement": ">=5.0.0 <6.0.0" }
-  ]
+  "description": "Installs and configures OpenStack Keystone (Identity)."
 }
index b0fd8d3..e5563f0 100644 (file)
@@ -43,13 +43,15 @@ describe 'basic keystone server with resources' do
         admin_token         => 'admin_token',
         enabled             => true,
       }
+      # "v2" admin and service
       class { '::keystone::roles::admin':
-        email    => 'test@example.tld',
-        password => 'a_big_secret',
+        email                  => 'test@example.tld',
+        password               => 'a_big_secret',
       }
       class { '::keystone::endpoint':
-        public_url => "http://127.0.0.1:5000/",
-        admin_url  => "http://127.0.0.1:35357/",
+        public_url     => "http://127.0.0.1:5000/",
+        admin_url      => "http://127.0.0.1:35357/",
+        default_domain => 'admin',
       }
       ::keystone::resource::service_identity { 'beaker-ci':
         service_type        => 'beaker',
@@ -60,6 +62,56 @@ describe 'basic keystone server with resources' do
         admin_url           => 'http://127.0.0.1:1234',
         internal_url        => 'http://127.0.0.1:1234',
       }
+      # v3 admin
+      # we don't use ::keystone::roles::admin but still create resources manually:
+      keystone_domain { 'admin_domain':
+        ensure      => present,
+        enabled     => true,
+        description => 'Domain for admin v3 users',
+      }
+      keystone_domain { 'service_domain':
+        ensure      => present,
+        enabled     => true,
+        description => 'Domain for admin v3 users',
+      }
+      keystone_tenant { 'servicesv3':
+        ensure      => present,
+        enabled     => true,
+        description => 'Tenant for the openstack services',
+        domain      => 'service_domain',
+      }
+      keystone_tenant { 'openstackv3':
+        ensure      => present,
+        enabled     => true,
+        description => 'admin tenant',
+        domain      => 'admin_domain',
+      }
+      keystone_user { 'adminv3':
+        ensure      => present,
+        enabled     => true,
+        tenant      => 'openstackv3', # note: don't have to use 'openstackv3::admin_domain' here since the tenant name 'openstackv3' is unique among all domains
+        email       => 'test@example.tld',
+        password    => 'a_big_secret',
+        domain      => 'admin_domain',
+      }
+      keystone_user_role { 'adminv3@openstackv3':
+        ensure => present,
+        roles  => ['admin'],
+      }
+      # service user exists only in the service_domain - must
+      # use v3 api
+      ::keystone::resource::service_identity { 'beaker-civ3':
+        service_type        => 'beakerv3',
+        service_description => 'beakerv3 service',
+        service_name        => 'beakerv3',
+        password            => 'secret',
+        tenant              => 'servicesv3',
+        public_url          => 'http://127.0.0.1:1234/v3',
+        admin_url           => 'http://127.0.0.1:1234/v3',
+        internal_url        => 'http://127.0.0.1:1234/v3',
+        user_domain         => 'service_domain',
+        project_domain      => 'service_domain',
+      }
       EOS
 
 
@@ -80,37 +132,94 @@ describe 'basic keystone server with resources' do
       it { should have_entry('1 0 * * * keystone-manage token_flush >>/var/log/keystone/keystone-tokenflush.log 2>&1').with_user('keystone') }
     end
 
-    describe 'test keystone user/tenant/service/role/endpoint resources' do
+    shared_examples_for 'keystone user/tenant/service/role/endpoint resources using v2 API' do |auth_creds|
+      it 'should find users in the default domain' do
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v2.0 --os-identity-api-version 2 user list") do |r|
+          expect(r.stdout).to match(/admin/)
+          expect(r.stderr).to be_empty
+        end
+      end
+      it 'should find tenants in the default domain' do
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v2.0 --os-identity-api-version 2 project list") do |r|
+          expect(r.stdout).to match(/openstack/)
+          expect(r.stderr).to be_empty
+        end
+      end
+      it 'should find beaker service' do
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v2.0 --os-identity-api-version 2 service list") do |r|
+          expect(r.stdout).to match(/beaker/)
+          expect(r.stderr).to be_empty
+        end
+      end
+      it 'should find admin role' do
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v2.0 --os-identity-api-version 2 role list") do |r|
+          expect(r.stdout).to match(/admin/)
+          expect(r.stderr).to be_empty
+        end
+      end
+      it 'should find beaker endpoints' do
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v2.0 --os-identity-api-version 2 endpoint list --long") do |r|
+          expect(r.stdout).to match(/1234/)
+          expect(r.stderr).to be_empty
+        end
+      end
+    end
+    shared_examples_for 'keystone user/tenant/service/role/endpoint resources using v3 API' do |auth_creds|
       it 'should find beaker user' do
-        shell('openstack --os-username admin --os-password a_big_secret --os-tenant-name openstack --os-auth-url http://127.0.0.1:5000/v2.0 user list') do |r|
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v3 --os-identity-api-version 3 user list") do |r|
           expect(r.stdout).to match(/beaker/)
           expect(r.stderr).to be_empty
         end
       end
       it 'should find services tenant' do
-        shell('openstack --os-username admin --os-password a_big_secret --os-tenant-name openstack --os-auth-url http://127.0.0.1:5000/v2.0 project list') do |r|
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v3 --os-identity-api-version 3 project list") do |r|
           expect(r.stdout).to match(/services/)
           expect(r.stderr).to be_empty
         end
       end
       it 'should find beaker service' do
-        shell('openstack --os-username admin --os-password a_big_secret --os-tenant-name openstack --os-auth-url http://127.0.0.1:5000/v2.0 service list') do |r|
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v3 --os-identity-api-version 3 service list") do |r|
           expect(r.stdout).to match(/beaker/)
           expect(r.stderr).to be_empty
         end
       end
       it 'should find admin role' do
-        shell('openstack --os-username admin --os-password a_big_secret --os-tenant-name openstack --os-auth-url http://127.0.0.1:5000/v2.0 role list') do |r|
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v3 --os-identity-api-version 3 role list") do |r|
           expect(r.stdout).to match(/admin/)
           expect(r.stderr).to be_empty
         end
       end
       it 'should find beaker endpoints' do
-        shell('openstack --os-username admin --os-password a_big_secret --os-tenant-name openstack --os-auth-url http://127.0.0.1:5000/v2.0 endpoint list --long') do |r|
+        shell("openstack #{auth_creds} --os-auth-url http://127.0.0.1:5000/v3 --os-identity-api-version 3 endpoint list") do |r|
           expect(r.stdout).to match(/1234/)
           expect(r.stderr).to be_empty
         end
       end
     end
+    describe 'with v2 admin with v2 credentials' do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v2 API',
+                       '--os-username admin --os-password a_big_secret --os-project-name openstack'
+    end
+    describe 'with v2 service with v2 credentials' do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v2 API',
+                       '--os-username beaker-ci --os-password secret --os-project-name services'
+    end
+    describe 'with v2 admin with v3 credentials' do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
+                       '--os-username admin --os-password a_big_secret --os-project-name openstack --os-user-domain-name Default --os-project-domain-name Default'
+    end
+    describe "with v2 service with v3 credentials" do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
+                       '--os-username beaker-ci --os-password secret --os-project-name services --os-user-domain-name Default --os-project-domain-name Default'
+    end
+    describe 'with v3 admin with v3 credentials' do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
+                       '--os-username adminv3 --os-password a_big_secret --os-project-name openstackv3 --os-user-domain-name admin_domain --os-project-domain-name admin_domain'
+    end
+    describe "with v3 service with v3 credentials" do
+      include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
+                       '--os-username beaker-civ3 --os-password secret --os-project-name servicesv3 --os-user-domain-name service_domain --os-project-domain-name service_domain'
+    end
+
   end
 end
index 217d791..59390ec 100644 (file)
@@ -48,4 +48,19 @@ describe 'keystone::endpoint' do
       )
     end
   end
+
+  describe 'with domain parameters' do
+
+    let :params do
+      { :user_domain    => 'userdomain',
+        :project_domain => 'projectdomain',
+        :default_domain => 'defaultdomain' }
+    end
+
+    it { is_expected.to contain_keystone__resource__service_identity('keystone').with(
+      :user_domain    => 'userdomain',
+      :project_domain => 'projectdomain',
+      :default_domain => 'defaultdomain'
+    )}
+  end
 end
index bbd6d95..f134d43 100644 (file)
@@ -54,8 +54,8 @@ describe 'keystone::roles::admin' do
     end
 
     it { is_expected.to contain_keystone_tenant('foobar').with(
-      :ensure  => 'present',
-      :enabled => true,
+      :ensure      => 'present',
+      :enabled     => true,
       :description => 'foobar description'
     )}
     it { is_expected.to contain_keystone_tenant('admin').with(
@@ -95,8 +95,8 @@ describe 'keystone::roles::admin' do
     before do
       let :params do
         {
-          :configure_user       => false,
-          :configure_user_role  => false
+          :configure_user      => false,
+          :configure_user_role => false
         }
       end
 
@@ -105,4 +105,72 @@ describe 'keystone::roles::admin' do
     end
   end
 
+  describe 'when specifying admin_user_domain and admin_project_domain' do
+    let :params do
+      {
+        :email                => 'foo@bar',
+        :password             => 'ChangeMe',
+        :admin_tenant         => 'admin_tenant',
+        :admin_user_domain    => 'admin_user_domain',
+        :admin_project_domain => 'admin_project_domain',
+      }
+    end
+    it { is_expected.to contain_keystone_user('admin').with(
+      :domain => 'admin_user_domain',
+      :tenant => 'admin_tenant'
+    )}
+    it { is_expected.to contain_keystone_tenant('admin_tenant').with(:domain => 'admin_project_domain') }
+    it { is_expected.to contain_keystone_domain('admin_user_domain') }
+    it { is_expected.to contain_keystone_domain('admin_project_domain') }
+
+  end
+
+  describe 'when specifying admin_user_domain and admin_project_domain' do
+    let :params do
+      {
+        :email                => 'foo@bar',
+        :password             => 'ChangeMe',
+        :admin_tenant         => 'admin_tenant::admin_project_domain',
+        :admin_user_domain    => 'admin_user_domain',
+        :admin_project_domain => 'admin_project_domain',
+      }
+    end
+    it { is_expected.to contain_keystone_user('admin').with(
+      :domain => 'admin_user_domain',
+      :tenant => 'admin_tenant::admin_project_domain'
+    )}
+    it { is_expected.to contain_keystone_tenant('admin_tenant::admin_project_domain').with(:domain => 'admin_project_domain') }
+    it { is_expected.to contain_keystone_domain('admin_user_domain') }
+    it { is_expected.to contain_keystone_domain('admin_project_domain') }
+
+  end
+
+  describe 'when specifying a service domain' do
+    let :params do
+      {
+        :email                  => 'foo@bar',
+        :password               => 'ChangeMe',
+        :service_tenant         => 'service_project',
+        :service_project_domain => 'service_domain'
+      }
+    end
+    it { is_expected.to contain_keystone_tenant('service_project').with(:domain => 'service_domain') }
+    it { is_expected.to contain_keystone_domain('service_domain') }
+
+  end
+
+  describe 'when specifying a service domain and service tenant domain' do
+    let :params do
+      {
+        :email                  => 'foo@bar',
+        :password               => 'ChangeMe',
+        :service_tenant         => 'service_project::service_domain',
+        :service_project_domain => 'service_domain'
+      }
+    end
+    it { is_expected.to contain_keystone_tenant('service_project::service_domain').with(:domain => 'service_domain') }
+    it { is_expected.to contain_keystone_domain('service_domain') }
+
+  end
+
 end
index 89c4fc5..c9537dc 100644 (file)
@@ -93,6 +93,7 @@ describe 'keystone' do
       'rabbit_host'           => '127.0.0.1',
       'rabbit_password'       => 'openstack',
       'rabbit_userid'         => 'admin',
+      'default_domain'        => 'other_domain',
     }
 
   httpd_params = {'service_name' => 'httpd'}.merge(default_params)
@@ -211,6 +212,10 @@ describe 'keystone' do
         is_expected.to contain_keystone_config('DEFAULT/public_workers').with_value('2')
       end
     end
+
+    if param_hash['default_domain']
+      it { is_expected.to contain_keystone_domain(param_hash['default_domain']).with(:is_default => true) }
+    end
   end
 
   [default_params, override_params].each do |param_hash|
@@ -845,6 +850,32 @@ describe 'keystone' do
     end
   end
 
+  describe 'when configuring default domain' do
+    describe 'with default config' do
+      let :params do
+        default_params
+      end
+      it { is_expected.to_not contain_exec('restart_keystone') }
+    end
+    describe 'with default domain and service is managed and enabled' do
+      let :params do
+        default_params.merge({
+          'default_domain'=> 'test',
+        })
+      end
+      it { is_expected.to contain_exec('restart_keystone') }
+    end
+    describe 'with default domain and service is not managed' do
+      let :params do
+        default_params.merge({
+          'default_domain' => 'test',
+          'manage_service' => false,
+        })
+      end
+      it { is_expected.to_not contain_exec('restart_keystone') }
+    end
+  end
+
   context 'on RedHat platforms' do
     let :facts do
       global_facts.merge({
diff --git a/3rdparty/modules/keystone/spec/defines/keystone_resource_authtoken_spec.rb b/3rdparty/modules/keystone/spec/defines/keystone_resource_authtoken_spec.rb
new file mode 100644 (file)
index 0000000..0689407
--- /dev/null
@@ -0,0 +1,198 @@
+require 'spec_helper'
+
+describe 'keystone::resource::authtoken' do
+
+  let (:title) { 'keystone_config' }
+
+  let :required_params do
+    { :username     => 'keystone',
+      :password     => 'secret',
+      :auth_url     => 'http://127.0.0.1:35357/',
+      :project_name => 'services' }
+  end
+
+  shared_examples 'shared examples' do
+
+    context 'with only required parameters' do
+      let :params do
+        required_params
+      end
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/username').with(
+        :value  => 'keystone',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/password').with(
+        :value  => 'secret',
+        :secret => true,
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/auth_plugin').with(
+        :value  => 'password',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/auth_url').with(
+        :value  => 'http://127.0.0.1:35357/',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_name').with(
+        :value  => 'services',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_domain_name').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_domain_name').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_domain_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_domain_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/domain_name').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/domain_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/trust_id').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/cacert').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/cert').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/key').with(
+        :ensure => 'absent',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/insecure').with(
+        :value => 'false',
+      )}
+
+    end
+
+    context 'when omitting a required parameter password' do
+      let :params do
+        required_params.delete(:password)
+      end
+      it { expect { is_expected.to raise_error(Puppet::Error) } }
+    end
+
+    context 'when specifying auth_url' do
+      let :params do
+        required_params.merge({:auth_url => 'https://host:11111/v3/'})
+      end
+      it { is_expected.to contain_keystone_config('keystone_authtoken/auth_url').with(
+        :value  => 'https://host:11111/v3/',
+      )}
+
+    end
+
+    context 'when specifying project and scope_domain' do
+      let :params do
+        required_params.merge({:domain_name => 'domain'})
+      end
+      it { expect { is_expected.to raise_error(Puppet::Error, 'Cannot specify both a project (project_name or project_id) and a domain (domain_name or domain_id)') } }
+    end
+
+    context 'when specifying neither project nor domain' do
+      let :params do
+        required_params.delete(:project_name)
+      end
+      it { expect { is_expected.to raise_error(Puppet::Error, 'Must specify either a project (project_name or project_id, for a project scoped token) or a domain (domain_name or domain_id, for a domain scoped token)') } }
+    end
+
+    context 'when specifying domain in name' do
+      let :params do
+        required_params.merge({
+          :username            => 'keystone::userdomain',
+          :project_name        => 'services::projdomain',
+          :default_domain_name => 'shouldnotuse'
+        })
+      end
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_domain_name').with(
+        :value => 'userdomain',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_domain_name').with(
+        :value => 'projdomain',
+      )}
+
+    end
+
+    context 'when specifying domain in parameters' do
+      let :params do
+        required_params.merge({
+          :username            => 'keystone::userdomain',
+          :user_domain_name    => 'realuserdomain',
+          :project_name        => 'services::projdomain',
+          :project_domain_name => 'realprojectdomain',
+          :default_domain_name => 'shouldnotuse'
+        })
+      end
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_domain_name').with(
+        :value => 'realuserdomain',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_domain_name').with(
+        :value => 'realprojectdomain',
+      )}
+
+    end
+
+    context 'when specifying only default domain' do
+      let :params do
+        required_params.merge({
+          :default_domain_name => 'defaultdomain'
+        })
+      end
+      it { is_expected.to contain_keystone_config('keystone_authtoken/user_domain_name').with(
+        :value => 'defaultdomain',
+      )}
+
+      it { is_expected.to contain_keystone_config('keystone_authtoken/project_domain_name').with(
+        :value => 'defaultdomain',
+      )}
+
+    end
+
+  end
+
+  context 'on a Debian osfamily' do
+    let :facts do
+      { :osfamily => "Debian" }
+    end
+
+    include_examples 'shared examples'
+  end
+
+  context 'on a RedHat osfamily' do
+    let :facts do
+      { :osfamily => 'RedHat' }
+    end
+
+    include_examples 'shared examples'
+  end
+end
index 1897963..63ef98a 100644 (file)
@@ -69,6 +69,69 @@ describe 'keystone::resource::service_identity' do
       it { expect { is_expected.to raise_error(Puppet::Error) } }
     end
 
+    context 'with user domain' do
+      let :params do
+        required_params.merge({:user_domain => 'userdomain'})
+      end
+      it { is_expected.to contain_keystone_domain('userdomain').with(
+        :ensure   => 'present',
+      )}
+      it { is_expected.to contain_keystone_user(title).with(
+        :ensure   => 'present',
+        :password => 'secrete',
+        :email    => 'neutron@localhost',
+        :tenant   => 'services',
+        :domain   => 'userdomain',
+      )}
+      it { is_expected.to contain_keystone_user_role("#{title}@services").with(
+        :ensure => 'present',
+        :roles  => ['admin'],
+      )}
+    end
+    context 'with user and project domain' do
+      let :params do
+        required_params.merge({
+          :user_domain => 'userdomain',
+          :project_domain => 'projdomain',
+        })
+      end
+      it { is_expected.to contain_keystone_user(title).with(
+        :ensure   => 'present',
+        :password => 'secrete',
+        :email    => 'neutron@localhost',
+        :tenant   => 'services',
+        :domain   => 'userdomain',
+      )}
+      it { is_expected.to contain_keystone_domain('userdomain').with(
+        :ensure   => 'present',
+      )}
+      it { is_expected.to contain_keystone_user_role("#{title}@services").with(
+        :ensure => 'present',
+        :roles  => ['admin'],
+      )}
+    end
+    context 'with default domain only' do
+      let :params do
+        required_params.merge({
+          :default_domain => 'defaultdomain',
+        })
+      end
+      it { is_expected.to contain_keystone_user(title).with(
+        :ensure   => 'present',
+        :password => 'secrete',
+        :email    => 'neutron@localhost',
+        :tenant   => 'services',
+        :domain   => 'defaultdomain',
+      )}
+      it { is_expected.to contain_keystone_domain('defaultdomain').with(
+        :ensure   => 'present',
+      )}
+      it { is_expected.to contain_keystone_user_role("#{title}@services").with(
+        :ensure => 'present',
+        :roles  => ['admin'],
+      )}
+    end
+
   end
 
   context 'on a Debian osfamily' do
index 2c2634a..429e807 100644 (file)
@@ -6,6 +6,7 @@ run_puppet_install_helper
 RSpec.configure do |c|
   # Project root
   proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+  modname = JSON.parse(open('metadata.json').read)['name'].split('-')[1]
 
   # Readable test descriptions
   c.formatter = :documentation
@@ -18,25 +19,38 @@ RSpec.configure do |c|
       # install git
       install_package host, 'git'
 
-      # clean out any module cruft
-      shell('rm -fr /etc/puppet/modules/*')
+      zuul_ref = ENV['ZUUL_REF']
+      zuul_branch = ENV['ZUUL_BRANCH']
+      zuul_url = ENV['ZUUL_URL']
+
+      repo = 'openstack/puppet-openstack-integration'
+
+      # Start out with clean moduledir, don't trust r10k to purge it
+      on host, "rm -rf /etc/puppet/modules/*"
+      # Install dependent modules via git or zuul
+      r = on host, "test -e /usr/zuul-env/bin/zuul-cloner", { :acceptable_exit_codes => [0,1] }
+      if r.exit_code == 0
+        zuul_clone_cmd = '/usr/zuul-env/bin/zuul-cloner '
+        zuul_clone_cmd += '--cache-dir /opt/git '
+        zuul_clone_cmd += "--zuul-ref #{zuul_ref} "
+        zuul_clone_cmd += "--zuul-branch #{zuul_branch} "
+        zuul_clone_cmd += "--zuul-url #{zuul_url} "
+        zuul_clone_cmd += "git://git.openstack.org #{repo}"
+        on host, zuul_clone_cmd
+      else
+        on host, "git clone https://git.openstack.org/#{repo} #{repo}"
+      end
+
+      on host, "ZUUL_REF=#{zuul_ref} ZUUL_BRANCH=#{zuul_branch} ZUUL_URL=#{zuul_url} bash #{repo}/install_modules.sh"
 
-      # install library modules from the forge
-      on host, puppet('module','install','puppetlabs-mysql'), { :acceptable_exit_codes => 0 }
-      on host, puppet('module','install','dprince/qpid'), { :acceptable_exit_codes => 0 }
-      on host, puppet('module','install','duritong/sysctl'), { :acceptable_exit_codes => 0 }
-      on host, puppet('module','install','puppetlabs-inifile'), { :acceptable_exit_codes => 0 }
-      on host, puppet('module','install','puppetlabs-rabbitmq'), { :acceptable_exit_codes => 0 }
-      on host, puppet('module','install','puppetlabs-apache'), { :acceptable_exit_codes => 0 }
+      # Install the module being tested
+      on host, "rm -fr /etc/puppet/modules/#{modname}"
+      puppet_module_install(:source => proj_root, :module_name => modname)
 
-      # install puppet modules from git, use master
-      shell('git clone https://git.openstack.org/openstack/puppet-openstacklib /etc/puppet/modules/openstacklib')
-      shell('git clone https://git.openstack.org/openstack/puppet-openstack_extras /etc/puppet/modules/openstack_extras')
+      on host, "rm -fr #{repo}"
 
-      # Install the module being tested
-      puppet_module_install(:source => proj_root, :module_name => 'keystone')
       # List modules installed to help with debugging
-      on hosts[0], puppet('module','list'), { :acceptable_exit_codes => 0 }
+      on host, puppet('module','list'), { :acceptable_exit_codes => 0 }
     end
   end
 end
diff --git a/3rdparty/modules/keystone/spec/unit/provider/keystone/util_spec.rb b/3rdparty/modules/keystone/spec/unit/provider/keystone/util_spec.rb
new file mode 100644 (file)
index 0000000..25f7b23
--- /dev/null
@@ -0,0 +1,29 @@
+require 'puppet'
+require 'spec_helper'
+require 'puppet/provider/keystone'
+require 'puppet/provider/keystone/util'
+
+describe "split_domain method" do
+  it 'should handle nil and empty strings' do
+    expect(Util.split_domain('')).to eq([nil, nil])
+    expect(Util.split_domain(nil)).to eq([nil, nil])
+  end
+  it 'should return name and no domain' do
+    expect(Util.split_domain('foo')).to eq(['foo', nil])
+    expect(Util.split_domain('foo::')).to eq(['foo', nil])
+  end
+  it 'should return name and domain' do
+    expect(Util.split_domain('foo::bar')).to eq(['foo', 'bar'])
+    expect(Util.split_domain('foo::bar::')).to eq(['foo', 'bar'])
+    expect(Util.split_domain('::foo::bar')).to eq(['::foo', 'bar'])
+    expect(Util.split_domain('::foo::bar::')).to eq(['::foo', 'bar'])
+    expect(Util.split_domain('foo::bar::baz')).to eq(['foo::bar', 'baz'])
+    expect(Util.split_domain('foo::bar::baz::')).to eq(['foo::bar', 'baz'])
+    expect(Util.split_domain('::foo::bar::baz')).to eq(['::foo::bar', 'baz'])
+    expect(Util.split_domain('::foo::bar::baz::')).to eq(['::foo::bar', 'baz'])
+  end
+  it 'should return domain only' do
+    expect(Util.split_domain('::foo')).to eq([nil, 'foo'])
+    expect(Util.split_domain('::foo::')).to eq([nil, 'foo'])
+  end
+end
diff --git a/3rdparty/modules/keystone/spec/unit/provider/keystone_domain/openstack_spec.rb b/3rdparty/modules/keystone/spec/unit/provider/keystone_domain/openstack_spec.rb
new file mode 100644 (file)
index 0000000..497f09b
--- /dev/null
@@ -0,0 +1,192 @@
+require 'puppet'
+require 'spec_helper'
+require 'puppet/provider/keystone_domain/openstack'
+
+provider_class = Puppet::Type.type(:keystone_domain).provider(:openstack)
+
+class Puppet::Provider::Keystone
+  def self.reset
+    @admin_endpoint = nil
+    @tenant_hash    = nil
+    @admin_token    = nil
+    @keystone_file  = nil
+    @domain_id_to_name = nil
+    @default_domain_id = nil
+    @domain_hash = nil
+  end
+end
+
+describe provider_class do
+
+  after :each do
+    provider_class.reset
+  end
+
+  shared_examples 'authenticated with environment variables' do
+    ENV['OS_USERNAME']     = 'test'
+    ENV['OS_PASSWORD']     = 'abc123'
+    ENV['OS_PROJECT_NAME'] = 'test'
+    ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v2.0'
+  end
+
+  describe 'when managing a domain' do
+
+    let(:domain_attrs) do
+      {
+        :name         => 'foo',
+        :description  => 'foo',
+        :ensure       => 'present',
+        :enabled      => 'True',
+      }
+    end
+
+    let(:resource) do
+      Puppet::Type::Keystone_domain.new(domain_attrs)
+    end
+
+    let(:provider) do
+      provider_class.new(resource)
+    end
+
+    it_behaves_like 'authenticated with environment variables' do
+      describe '#create' do
+        it 'creates a domain' do
+          # keystone.conf
+          File.expects(:exists?).returns(true)
+          kcmock = {
+            'identity' => {'default_domain_id' => ' default'}
+          }
+          Puppet::Util::IniConfig::File.expects(:new).returns(kcmock)
+          kcmock.expects(:read).with('/etc/keystone/keystone.conf')
+          provider.class.expects(:openstack)
+                        .with('domain', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo'])
+                        .returns('id="1cb05cfed7c24279be884ba4f6520262"
+name="foo"
+description="foo"
+enabled=True
+')
+          provider.create
+          expect(provider.exists?).to be_truthy
+        end
+
+      end
+
+      describe '#destroy' do
+        it 'destroys a domain' do
+          provider.instance_variable_get('@property_hash')[:id] = 'my-domainid'
+          # keystone.conf
+          File.expects(:exists?).returns(true)
+          kcmock = {
+            'identity' => {'default_domain_id' => ' default'}
+          }
+          Puppet::Util::IniConfig::File.expects(:new).returns(kcmock)
+          kcmock.expects(:read).with('/etc/keystone/keystone.conf')
+          provider.class.expects(:openstack)
+                        .with('domain', 'set', ['foo', '--disable'])
+          provider.class.expects(:openstack)
+                        .with('domain', 'delete', 'foo')
+          provider.destroy
+          expect(provider.exists?).to be_falsey
+        end
+
+      end
+
+      describe '#instances' do
+        it 'finds every domain' do
+          provider.class.expects(:openstack)
+                        .with('domain', 'list', '--quiet', '--format', 'csv', [])
+                        .returns('"ID","Name","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo",True
+')
+          instances = provider_class.instances
+          expect(instances.count).to eq(1)
+        end
+      end
+
+      describe '#create default' do
+        let(:domain_attrs) do
+          {
+            :name         => 'foo',
+            :description  => 'foo',
+            :ensure       => 'present',
+            :enabled      => 'True',
+            :is_default   => 'True',
+          }
+        end
+
+        it 'creates a default domain' do
+          File.expects(:exists?).returns(true)
+          mock = {
+            'identity' => {'default_domain_id' => ' default'}
+          }
+          Puppet::Util::IniConfig::File.expects(:new).returns(mock)
+          mock.expects(:read).with('/etc/keystone/keystone.conf')
+          mock.expects(:store)
+          provider.class.expects(:openstack)
+                        .with('domain', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo'])
+                        .returns('id="1cb05cfed7c24279be884ba4f6520262"
+name="foo"
+description="foo"
+enabled=True
+')
+          provider.create
+          expect(provider.exists?).to be_truthy
+          expect(mock['identity']['default_domain_id']).to eq('1cb05cfed7c24279be884ba4f6520262')
+        end
+      end
+
+      describe '#destroy default' do
+        it 'destroys a default domain' do
+          provider.instance_variable_get('@property_hash')[:is_default] = true
+          provider.instance_variable_get('@property_hash')[:id] = 'my-domainid'
+          # keystone.conf
+          File.expects(:exists?).returns(true)
+          kcmock = {
+            'identity' => {'default_domain_id' => ' my-domainid'}
+          }
+          Puppet::Util::IniConfig::File.expects(:new).returns(kcmock)
+          kcmock.expects(:read).with('/etc/keystone/keystone.conf')
+          kcmock.expects(:store)
+          provider.class.expects(:openstack)
+                        .with('domain', 'set', ['foo', '--disable'])
+          provider.class.expects(:openstack)
+                        .with('domain', 'delete', 'foo')
+          provider.destroy
+          expect(provider.exists?).to be_falsey
+          expect(kcmock['identity']['default_domain_id']).to eq('default')
+        end
+      end
+
+      describe '#flush' do
+        let(:domain_attrs) do
+          {
+            :name         => 'foo',
+            :description  => 'new description',
+            :ensure       => 'present',
+            :enabled      => 'True',
+            :is_default   => 'True',
+          }
+        end
+
+        it 'changes the description' do
+          provider.class.expects(:openstack)
+                        .with('domain', 'set', ['foo', '--description', 'new description'])
+          provider.description=('new description')
+          provider.flush
+        end
+
+        it 'changes is_default' do
+          # keystone.conf
+          File.expects(:exists?).returns(true)
+          kcmock = {
+            'identity' => {'default_domain_id' => ' my-domainid'}
+          }
+          Puppet::Util::IniConfig::File.expects(:new).returns(kcmock)
+          kcmock.expects(:read).with('/etc/keystone/keystone.conf')
+          provider.is_default=(true)
+          provider.flush
+        end
+      end
+    end
+  end
+end
index 09e229b..6765e32 100644 (file)
@@ -32,12 +32,7 @@ describe provider_class do
 
       describe '#create' do
         it 'creates a role' do
-          provider.class.stubs(:openstack)
-                        .with('role', 'list', '--quiet', '--format', 'csv', [])
-                        .returns('"ID","Name"
-"1cb05cfed7c24279be884ba4f6520262","foo"
-')
-          provider.class.stubs(:openstack)
+          provider.class.expects(:openstack)
                         .with('role', 'create', '--format', 'shell', 'foo')
                         .returns('name="foo"')
           provider.create
@@ -47,10 +42,7 @@ describe provider_class do
 
       describe '#destroy' do
         it 'destroys a role' do
-          provider.class.stubs(:openstack)
-                        .with('role', 'list', '--quiet', '--format', 'csv', [])
-                        .returns('"ID","Name"')
-          provider.class.stubs(:openstack)
+          provider.class.expects(:openstack)
                         .with('role', 'delete', [])
           provider.destroy
           expect(provider.exists?).to be_falsey
@@ -61,9 +53,6 @@ describe provider_class do
       describe '#exists' do
         context 'when role does not exist' do
           subject(:response) do
-            provider.class.stubs(:openstack)
-                          .with('role', 'list', '--quiet', '--format', 'csv', [])
-                        .returns('"ID","Name"')
             response = provider.exists?
           end
           it { is_expected.to be_falsey }
@@ -72,7 +61,7 @@ describe provider_class do
 
       describe '#instances' do
         it 'finds every role' do
-          provider.class.stubs(:openstack)
+          provider.class.expects(:openstack)
                         .with('role', 'list', '--quiet', '--format', 'csv', [])
                         .returns('"ID","Name"
 "1cb05cfed7c24279be884ba4f6520262","foo"
index 5a299a5..f685a80 100644 (file)
@@ -10,7 +10,7 @@ describe provider_class do
     ENV['OS_USERNAME']     = 'test'
     ENV['OS_PASSWORD']     = 'abc123'
     ENV['OS_PROJECT_NAME'] = 'test'
-    ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v2.0'
+    ENV['OS_AUTH_URL']     = 'http://127.0.0.1:5000/v3'
   end
 
   describe 'when managing a service' do
@@ -41,7 +41,7 @@ describe provider_class do
 "1cb05cfed7c24279be884ba4f6520262","foo","foo","foo"
 ')
           provider.class.stubs(:openstack)
-                        .with('service', 'create', '--format', 'shell', ['--description', 'foo', '--type', 'foo', 'foo'])
+                        .with('service', 'create', '--format', 'shell', ['foo', '--name', 'foo', '--description', 'foo'])
                         .returns('description="foo"
 enabled="True"
 id="8f0dd4c0abc44240998fbb3f5089ecbf"
index 4981f1e..44d265f 100644 (file)
@@ -6,13 +6,16 @@ require 'tempfile'
 klass = Puppet::Provider::Keystone
 
 class Puppet::Provider::Keystone
-  @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
+  @credentials = Puppet::Provider::Openstack::CredentialsV3.new
 
   def self.reset
     @admin_endpoint = nil
     @tenant_hash    = nil
     @admin_token    = nil
     @keystone_file  = nil
+    @domain_id_to_name = nil
+    @default_domain_id = nil
+    @domain_hash = nil
   end
 end
 
@@ -57,7 +60,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v3/')
     end
 
     it 'should use localhost in the admin endpoint if bind_host is 0.0.0.0' do
@@ -65,7 +68,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v3/')
     end
 
     it 'should use [::1] in the admin endpoint if bind_host is ::0' do
@@ -73,7 +76,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('http://[::1]:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('http://[::1]:35357/v3/')
     end
 
     it 'should use localhost in the admin endpoint if bind_host is unspecified' do
@@ -81,7 +84,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v3/')
     end
 
     it 'should use https if ssl is enabled' do
@@ -89,7 +92,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('https://192.168.56.210:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('https://192.168.56.210:35357/v3/')
     end
 
     it 'should use http if ssl is disabled' do
@@ -97,7 +100,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v3/')
     end
 
     it 'should use the defined admin_endpoint if available' do
@@ -105,7 +108,7 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v3/')
     end
 
     it 'should handle an admin_endpoint with a trailing slash' do
@@ -113,9 +116,58 @@ describe Puppet::Provider::Keystone do
       File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
       Puppet::Util::IniConfig::File.expects(:new).returns(mock)
       mock.expects(:read).with('/etc/keystone/keystone.conf')
-      expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v2.0/')
+      expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v3/')
     end
 
   end
 
+  describe 'when using domains' do
+    it 'name_and_domain should return the resource domain' do
+      expect(klass.name_and_domain('foo::in_name', 'from_resource', 'default')).to eq(['foo', 'from_resource'])
+    end
+    it 'name_and_domain should return the default domain' do
+      expect(klass.name_and_domain('foo', nil, 'default')).to eq(['foo', 'default'])
+    end
+    it 'name_and_domain should return the domain part of the name' do
+      expect(klass.name_and_domain('foo::in_name', nil, 'default')).to eq(['foo', 'in_name'])
+    end
+    it 'should return the default domain name using the default_domain_id from keystone.conf' do
+      ENV['OS_USERNAME']     = 'test'
+      ENV['OS_PASSWORD']     = 'abc123'
+      ENV['OS_PROJECT_NAME'] = 'test'
+      ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v3'
+      mock = {
+        'DEFAULT' => {
+          'admin_endpoint' => 'http://127.0.0.1:35357',
+          'admin_token'    => 'admin_token'
+        },
+        'identity' => {'default_domain_id' => 'somename'}
+      }
+      File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
+      Puppet::Util::IniConfig::File.expects(:new).returns(mock)
+      mock.expects(:read).with('/etc/keystone/keystone.conf')
+      klass.expects(:openstack)
+           .with('domain', 'list', '--quiet', '--format', 'csv', [])
+           .returns('"ID","Name","Enabled","Description"
+"somename","SomeName",True,"default domain"
+')
+      expect(klass.name_and_domain('foo')).to eq(['foo', 'SomeName'])
+    end
+    it 'should return Default if default_domain_id is not configured' do
+      ENV['OS_USERNAME']     = 'test'
+      ENV['OS_PASSWORD']     = 'abc123'
+      ENV['OS_PROJECT_NAME'] = 'test'
+      ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v3'
+      mock = {}
+      Puppet::Util::IniConfig::File.expects(:new).returns(mock)
+      File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
+      mock.expects(:read).with('/etc/keystone/keystone.conf')
+      klass.expects(:openstack)
+           .with('domain', 'list', '--quiet', '--format', 'csv', [])
+           .returns('"ID","Name","Enabled","Description"
+"default","Default",True,"default domain"
+')
+      expect(klass.name_and_domain('foo')).to eq(['foo', 'Default'])
+    end
+  end
 end
index 1dec49e..fd17e42 100644 (file)
@@ -4,87 +4,233 @@ require 'puppet/provider/keystone_tenant/openstack'
 
 provider_class = Puppet::Type.type(:keystone_tenant).provider(:openstack)
 
+class Puppet::Provider::Keystone
+  def self.reset
+    @admin_endpoint = nil
+    @tenant_hash    = nil
+    @admin_token    = nil
+    @keystone_file  = nil
+    @domain_id_to_name = nil
+    @default_domain_id = nil
+    @domain_hash = nil
+  end
+end
+
 describe provider_class do
 
+  after :each do
+    provider_class.reset
+  end
+
+  let(:tenant_attrs) do
+    {
+      :name         => 'foo',
+      :description  => 'foo',
+      :ensure       => 'present',
+      :enabled      => 'True',
+    }
+  end
+
+  let(:resource) do
+    Puppet::Type::Keystone_tenant.new(tenant_attrs)
+  end
+
+  let(:provider) do
+    provider_class.new(resource)
+  end
+
+  def before_hook(domainlist)
+    if domainlist
+      provider.class.expects(:openstack).once
+                    .with('domain', 'list', '--quiet', '--format', 'csv', [])
+                    .returns('"ID","Name","Enabled","Description"
+"foo_domain_id","foo_domain",True,"foo domain"
+"bar_domain_id","bar_domain",True,"bar domain"
+"another_domain_id","another_domain",True,"another domain"
+"disabled_domain_id","disabled_domain",False,"disabled domain"
+"default","Default",True,"the default domain"
+')
+    end
+  end
+
+  before :each, :domainlist => true do
+    before_hook(true)
+  end
+
+  before :each, :domainlist => false do
+    before_hook(false)
+  end
+
   shared_examples 'authenticated with environment variables' do
     ENV['OS_USERNAME']     = 'test'
     ENV['OS_PASSWORD']     = 'abc123'
     ENV['OS_PROJECT_NAME'] = 'test'
-    ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v2.0'
+    ENV['OS_AUTH_URL']     = 'http://127.0.0.1:35357/v3'
   end
 
   describe 'when managing a tenant' do
 
-    let(:tenant_attrs) do
-      {
-        :name         => 'foo',
-        :description  => 'foo',
-        :ensure       => 'present',
-        :enabled      => 'True',
-      }
-    end
-
-    let(:resource) do
-      Puppet::Type::Keystone_tenant.new(tenant_attrs)
-    end
-
-    let(:provider) do
-      provider_class.new(resource)
-    end
-
     it_behaves_like 'authenticated with environment variables' do
-      describe '#create' do
+      describe '#create', :domainlist => true do
         it 'creates a tenant' do
-          provider.class.stubs(:openstack)
-                        .with('project', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Description","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo",True
-')
-          provider.class.stubs(:openstack)
-                        .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo'])
+          provider.class.expects(:openstack)
+                        .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo', '--domain', 'Default'])
                         .returns('description="foo"
 enabled="True"
 name="foo"
+id="foo"
+domain_id="foo_domain_id"
 ')
           provider.create
           expect(provider.exists?).to be_truthy
         end
       end
 
-      describe '#destroy' do
+      describe '#destroy', :domainlist => false do
         it 'destroys a tenant' do
-          provider.class.stubs(:openstack)
-                        .with('project', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Description","Enabled"')
-          provider.class.stubs(:openstack)
-                        .with('project', 'delete', [])
+          provider.instance_variable_get('@property_hash')[:id] = 'my-project-id'
+          provider.class.expects(:openstack)
+                        .with('project', 'delete', 'my-project-id')
           provider.destroy
           expect(provider.exists?).to be_falsey
         end
       end
 
-      context 'when tenant does not exist' do
+      context 'when tenant does not exist', :domainlist => false do
         subject(:response) do
-          provider.class.stubs(:openstack)
-                        .with('project', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Description","Enabled"')
           response = provider.exists?
         end
 
-        it { is_expected.to be_falsey }
+        it { expect(response).to be_falsey }
       end
 
-      describe '#instances' do
+      describe '#instances', :domainlist => true do
         it 'finds every tenant' do
-          provider.class.stubs(:openstack)
+          provider.class.expects(:openstack)
                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
-                       .returns('"ID","Name","Description","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo",True
+                       .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","foo","bar_domain_id","foo",True
 ')
-          instances = Puppet::Type::Keystone_tenant::ProviderOpenstack.instances
-          expect(instances.count).to eq(1)
+          instances = provider.class.instances
+          expect(instances[0].name).to eq('foo')
+          expect(instances[0].domain).to eq('bar_domain')
+          expect(instances[1].name).to eq('foo::foo_domain')
         end
       end
     end
+
+    describe 'v3 domains with no domain in resource', :domainlist => true do
+
+      let(:tenant_attrs) do
+        {
+          :name         => 'foo',
+          :description  => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True'
+        }
+      end
+
+      it 'adds default domain to commands' do
+        mock = {
+          'identity' => {'default_domain_id' => 'foo_domain_id'}
+        }
+        Puppet::Util::IniConfig::File.expects(:new).returns(mock)
+        File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
+        mock.expects(:read).with('/etc/keystone/keystone.conf')
+        provider.class.expects(:openstack)
+          .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo', '--domain', 'foo_domain'])
+          .returns('description="foo"
+enabled="True"
+name="foo"
+id="project-id"
+domain_id="foo_domain_id"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("project-id")
+      end
+
+    end
+
+    describe 'v3 domains with domain in resource', :domainlist => false do
+
+      let(:tenant_attrs) do
+        {
+          :name         => 'foo',
+          :description  => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :domain       => 'foo_domain'
+        }
+      end
+
+      it 'uses given domain in commands' do
+        provider.class.expects(:openstack)
+          .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo', '--domain', 'foo_domain'])
+          .returns('description="foo"
+enabled="True"
+name="foo"
+id="project-id"
+domain_id="foo_domain_id"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("project-id")
+      end
+    end
+
+    describe 'v3 domains with domain in name/title', :domainlist => false do
+
+      let(:tenant_attrs) do
+        {
+          :name         => 'foo::foo_domain',
+          :description  => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True'
+        }
+      end
+
+      it 'uses given domain in commands' do
+        provider.class.expects(:openstack)
+          .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo', '--domain', 'foo_domain'])
+          .returns('description="foo"
+enabled="True"
+name="foo"
+id="project-id"
+domain_id="foo_domain_id"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("project-id")
+      end
+    end
+
+    describe 'v3 domains with domain in name/title and in resource', :domainlist => false do
+
+      let(:tenant_attrs) do
+        {
+          :name         => 'foo::bar_domain',
+          :description  => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :domain       => 'foo_domain'
+        }
+      end
+
+      it 'uses given domain in commands' do
+        provider.class.expects(:openstack)
+          .with('project', 'create', '--format', 'shell', ['foo', '--enable', '--description', 'foo', '--domain', 'foo_domain'])
+          .returns('description="foo"
+enabled="True"
+name="foo"
+id="project-id"
+domain_id="foo_domain_id"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("project-id")
+      end
+    end
   end
 end
index 3f545d0..d7de008 100644 (file)
@@ -1,9 +1,14 @@
 require 'puppet'
 require 'spec_helper'
 require 'puppet/provider/keystone_user/openstack'
+require 'puppet/provider/openstack'
 
 provider_class = Puppet::Type.type(:keystone_user).provider(:openstack)
 
+def project_class
+  Puppet::Type.type(:keystone_tenant).provider(:openstack)
+end
+
 describe provider_class do
 
   shared_examples 'authenticated with environment variables' do
@@ -21,6 +26,7 @@ describe provider_class do
       :password     => 'foo',
       :tenant       => 'foo',
       :email        => 'foo@example.com',
+      :domain       => 'foo_domain',
     }
   end
 
@@ -32,22 +38,95 @@ describe provider_class do
     provider_class.new(resource)
   end
 
+  def before_hook(delete, missing, noproject, user_cached)
+    provider.class.expects(:openstack).once
+                  .with('domain', 'list', '--quiet', '--format', 'csv', [])
+                  .returns('"ID","Name","Enabled","Description"
+"foo_domain_id","foo_domain",True,"foo domain"
+"bar_domain_id","bar_domain",True,"bar domain"
+"another_domain_id","another_domain",True,"another domain"
+"disabled_domain_id","disabled_domain",False,"disabled domain"
+')
+    if user_cached
+      return # using cached user, so no user list
+    end
+    if noproject
+      project = ''
+    else
+      project = 'foo'
+    end
+    # delete will call the search again and should not return the deleted user
+    foo_returns = ['"ID","Name","Project Id","Domain","Description","Email","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo",' + project + ',"foo_domain_id","foo description","foo@example.com",True
+"2cb05cfed7c24279be884ba4f6520262","foo",' + project + ',"bar_domain_id","foo description","foo@example.com",True
+"3cb05cfed7c24279be884ba4f6520262","foo",' + project + ',"another_domain_id","foo description","foo@example.com",True
+']
+    nn = 1
+    if delete
+      nn = 2
+      foo_returns << ''
+    end
+    if missing
+      foo_returns = ['']
+    end
+    provider.class.expects(:openstack).times(nn)
+                  .with('user', 'list', '--quiet', '--format', 'csv', ['--long'])
+                  .returns(*foo_returns)
+  end
+
+  before :each, :default => true do
+    before_hook(false, false, false, false)
+  end
+  before :each, :delete => true do
+    before_hook(true, false, false, false)
+  end
+  before :each, :missing => true do
+    before_hook(false, true, false, false)
+  end
+  before :each, :noproject => true do
+    before_hook(false, false, true, false)
+  end
+  before :each, :default_https => true do
+    before_hook(false, false, false, false)
+  end
+  before :each, :user_cached => true do
+    before_hook(false, false, false, true)
+  end
+  before :each, :nohooks => true do
+    # do nothing
+  end
+
   describe 'when managing a user' do
     it_behaves_like 'authenticated with environment variables' do
       describe '#create' do
         it 'creates a user' do
-          provider.class.stubs(:openstack)
-                        .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Project","Email","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo","foo@example.com",True
+          project_class.expects(:openstack).once
+                       .with('domain', 'list', '--quiet', '--format', 'csv', [])
+                       .returns('"ID","Name","Enabled","Description"
+"foo_domain_id","foo_domain",True,"foo domain"
+"bar_domain_id","bar_domain",True,"bar domain"
+"another_domain_id","another_domain",True,"another domain"
+"disabled_domain_id","disabled_domain",False,"disabled domain"
+')
+          project_class.expects(:openstack)
+                       .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                       .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","foo","bar_domain_id","foo",True
+')
+          provider.class.expects(:openstack)
+                        .with('role', 'show', '--format', 'shell', '_member_')
+                        .returns('
+name="_member_"
 ')
-          provider.class.stubs(:openstack)
-                        .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--project', 'foo', '--email', 'foo@example.com'])
+          provider.class.expects(:openstack)
+                        .with('role', 'add', ['_member_', '--project', '2cb05cfed7c24279be884ba4f6520262', '--user', '12b23f07d4a3448d8189521ab09610b0'])
+          provider.class.expects(:openstack)
+                        .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'foo_domain'])
                         .returns('email="foo@example.com"
 enabled="True"
 id="12b23f07d4a3448d8189521ab09610b0"
 name="foo"
-project_id="5e2001b2248540f191ff22627dc0c2d7"
 username="foo"
 ')
           provider.create
@@ -57,11 +136,9 @@ username="foo"
 
       describe '#destroy' do
         it 'destroys a user' do
-          provider.class.stubs(:openstack)
-                        .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Project","Email","Enabled"')
-          provider.class.stubs(:openstack)
-                        .with('user', 'delete', [])
+          provider.instance_variable_get('@property_hash')[:id] = 'my-user-id'
+          provider.class.expects(:openstack)
+                        .with('user', 'delete', 'my-user-id')
           provider.destroy
           expect(provider.exists?).to be_falsey
         end
@@ -71,9 +148,6 @@ username="foo"
       describe '#exists' do
         context 'when user does not exist' do
           subject(:response) do
-            provider.class.stubs(:openstack)
-                          .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                          .returns('"ID","Name","Project","Email","Enabled"')
             response = provider.exists?
           end
 
@@ -81,134 +155,133 @@ username="foo"
         end
       end
 
-      describe '#instances' do
+      describe '#instances', :default => true do
         it 'finds every user' do
-          provider.class.stubs(:openstack)
-                        .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Project","Email","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo","foo@example.com",True
-')
-          instances = Puppet::Type::Keystone_user::ProviderOpenstack.instances
-          expect(instances.count).to eq(1)
+          instances = provider.class.instances
+          expect(instances.count).to eq(3)
+          expect(instances[0].name).to eq('foo')
+          expect(instances[0].domain).to eq('another_domain')
+          expect(instances[1].name).to eq('foo::foo_domain')
+          expect(instances[2].name).to eq('foo::bar_domain')
         end
       end
 
       describe '#tenant' do
-        it 'gets the tenant with default backend' do
-          provider.class.stubs(:openstack)
-                        .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Project","Email","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo","foo@example.com",True
+        it 'gets the tenant with default backend', :nohooks => true do
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
 ')
-          provider.class.stubs(:openstack)
-                        .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                        .returns('"ID","Name","Project","User"
-"9fe2ff9ee4384b1894a90878d3e92bab","_member_","foo","foo"
+          provider.class.expects(:openstack)
+                        .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                        .returns('"ID","Name","Domain ID","Description","Enabled"
+"foo_project_id1","foo","foo_domain_id","",True
 ')
+          provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
           tenant = provider.tenant
           expect(tenant).to eq('foo')
         end
 
-        it 'gets the tenant with LDAP backend' do
-          provider.class.stubs(:openstack)
-                        .with('user', 'list', '--quiet', '--format', 'csv', '--long')
-                        .returns('"ID","Name","Project","Email","Enabled"
-"1cb05cfed7c24279be884ba4f6520262","foo","","foo@example.com",True
+        it 'gets the tenant with LDAP backend', :nohooks => true do
+          provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
 ')
           provider.class.expects(:openstack)
-                        .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                        .returns('"ID","Name","Project","User"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo","foo"
+                        .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                        .returns('"ID","Name","Domain ID","Description","Enabled"
+"foo_project_id1","foo","foo_domain_id","",True
+"bar_project_id2","bar","bar_domain_id","",True
+"foo_project_id2","foo","another_domain_id","",True
 ')
           tenant = provider.tenant
           expect(tenant).to eq('foo')
         end
       end
-
       describe '#tenant=' do
-        context 'when using default backend' do
+        context 'when using default backend', :nohooks => true do
           it 'sets the tenant' do
+            provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+            provider.instance_variable_get('@property_hash')[:domain] = 'foo_domain'
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
             provider.class.expects(:openstack)
-                          .with('user', 'set', ['foo', '--project', 'bar'])
+                          .with('role', 'show', '--format', 'shell', '_member_')
+                          .returns('name="_member_"')
             provider.class.expects(:openstack)
-                          .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'bar'])
-                          .returns('"ID","Name","Project","User"
-"9fe2ff9ee4384b1894a90878d3e92bab","_member_","bar","foo"
-')
+                          .with('role', 'add', ['_member_', '--project', '2cb05cfed7c24279be884ba4f6520262', '--user', '1cb05cfed7c24279be884ba4f6520262'])
             provider.tenant=('bar')
           end
         end
-
-        context 'when using LDAP read-write backend' do
+        context 'when using LDAP read-write backend', :nohooks => true do
           it 'sets the tenant when _member_ role exists' do
-            provider.class.expects(:openstack)
-                          .with('user', 'set', ['foo', '--project', 'bar'])
-            provider.class.expects(:openstack)
-                          .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'bar'])
-                          .returns('')
-            provider.class.expects(:openstack)
-                          .with('role', 'show', '--format', 'shell', ['_member_'])
-                          .returns('id="9fe2ff9ee4384b1894a90878d3e92bab"
-name="_member_"
+            provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+            provider.instance_variable_get('@property_hash')[:domain] = 'foo_domain'
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
 ')
             provider.class.expects(:openstack)
-                          .with('role', 'add', ['_member_', '--project', 'bar', '--user', 'foo'])
+                          .with('role', 'show', '--format', 'shell', '_member_')
+                          .returns('name="_member_"')
+            provider.class.expects(:openstack)
+                          .with('role', 'add', ['_member_', '--project', '2cb05cfed7c24279be884ba4f6520262', '--user', '1cb05cfed7c24279be884ba4f6520262'])
             provider.tenant=('bar')
           end
           it 'sets the tenant when _member_ role does not exist' do
+            provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+            provider.instance_variable_get('@property_hash')[:domain] = 'foo_domain'
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
             provider.class.expects(:openstack)
-                          .with('user', 'set', ['foo', '--project', 'bar'])
-            provider.class.expects(:openstack)
-                          .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'bar'])
-                          .returns('')
-            provider.class.expects(:openstack)
-                          .with('role', 'show', '--format', 'shell', ['_member_'])
+                          .with('role', 'show', '--format', 'shell', '_member_')
                           .raises(Puppet::ExecutionFailure, 'no such role _member_')
             provider.class.expects(:openstack)
-                          .with('role', 'create', '--format', 'shell', ['_member_'])
+                          .with('role', 'create', '--format', 'shell', '_member_')
                           .returns('name="_member_"')
             provider.class.expects(:openstack)
-                          .with('role', 'add', ['_member_', '--project', 'bar', '--user', 'foo'])
-                          .returns('id="8wr2ff9ee4384b1894a90878d3e92bab"
-name="_member_"
-')
+                          .with('role', 'add', ['_member_', '--project', '2cb05cfed7c24279be884ba4f6520262', '--user', '1cb05cfed7c24279be884ba4f6520262'])
             provider.tenant=('bar')
           end
         end
-
-# This doesn't make sense, need to clarify what's happening with LDAP mock
-=begin
-        context 'when using LDAP read-only backend' do
+        context 'when using LDAP read-only backend', :nohooks => true do
           it 'sets the tenant when _member_ role exists' do
-            provider.class.expects(:openstack)
-                          .with('user', 'set', [['foo', '--project', 'bar']])
-                          .raises(Puppet::ExecutionFailure, 'You are not authorized to perform the requested action: LDAP user update')
-            provider.class.expects(:openstack)
-                           .with('user role', 'list', '--quiet', '--format', 'csv', [['foo', '--project', 'bar']])
-                           .returns('')
-            provider.class.expects(:openstack)
-                          .with('role', 'show', '--format', 'shell', [['_member_']])
-                          .returns('id="9fe2ff9ee4384b1894a90878d3e92bab"
-name="_member_"
+            provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+            provider.instance_variable_get('@property_hash')[:domain] = 'foo_domain'
+            project_class.expects(:openstack)
+                         .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                         .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
 ')
             provider.class.expects(:openstack)
-                          .with('role', 'add', [['_member_', '--project', 'bar', '--user', 'foo']])
-            provider.tenant=('bar')
-          end
-
-          it 'sets the tenant and gets an unexpected exception message' do
+                          .with('role', 'show', '--format', 'shell', '_member_')
+                          .returns('name="_member_"')
             provider.class.expects(:openstack)
-                          .with('user', 'set', [['foo', '--project', 'bar']])
-                          .raises(Puppet::ExecutionFailure, 'unknown error message')
-            expect{ provider.tenant=('bar') }.to raise_error(Puppet::ExecutionFailure, /unknown error message/)
+                          .with('role', 'add', ['_member_', '--project', '2cb05cfed7c24279be884ba4f6520262', '--user', '1cb05cfed7c24279be884ba4f6520262'])
+            provider.tenant=('bar')
           end
         end
-=end
       end
     end
   end
 
-  describe "#password" do
+  describe "#password", :nohooks => true do
     let(:user_attrs) do
       {
         :name         => 'foo',
@@ -217,6 +290,7 @@ name="_member_"
         :password     => 'foo',
         :tenant       => 'foo',
         :email        => 'foo@example.com',
+        :domain       => 'foo_domain',
       }
     end
 
@@ -229,12 +303,26 @@ name="_member_"
     end
 
     shared_examples 'with auth-url environment variable' do
-      ENV['OS_AUTH_URL'] = 'http://localhost:5000'
+      ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000'
     end
 
     it_behaves_like 'with auth-url environment variable' do
       it 'checks the password' do
-        Puppet::Provider::Openstack.stubs(:openstack)
+        provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+        mockcreds = {}
+        Puppet::Provider::Openstack::CredentialsV3.expects(:new).returns(mockcreds)
+        mockcreds.expects(:auth_url=).with('http://127.0.0.1:5000')
+        mockcreds.expects(:password=).with('foo')
+        mockcreds.expects(:username=).with('foo')
+        mockcreds.expects(:user_id=).with('1cb05cfed7c24279be884ba4f6520262')
+        mockcreds.expects(:project_id=).with('project-id-1')
+        mockcreds.expects(:to_env).returns(mockcreds)
+        Puppet::Provider::Openstack.expects(:openstack)
+                      .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                      .returns('"ID","Name","Domain ID","Description","Enabled"
+"project-id-1","foo","foo_domain_id","foo",True
+')
+        Puppet::Provider::Openstack.expects(:openstack)
                       .with('token', 'issue', ['--format', 'value'])
                       .returns('2015-05-14T04:06:05Z
 e664a386befa4a30878dcef20e79f167
@@ -246,13 +334,48 @@ ac43ec53d5a74a0b9f51523ae41a29f0
       end
 
       it 'fails the password check' do
-        Puppet::Provider::Openstack.stubs(:openstack)
+        provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+        Puppet::Provider::Openstack.expects(:openstack)
+                      .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                      .returns('"ID","Name","Domain ID","Description","Enabled"
+"project-id-1","foo","foo_domain_id","foo",True
+')
+        Puppet::Provider::Openstack.expects(:openstack)
                       .with('token', 'issue', ['--format', 'value'])
                       .raises(Puppet::ExecutionFailure, 'HTTP 401 invalid authentication')
         password = provider.password
         expect(password).to eq(nil)
       end
+
+      it 'checks the password with domain scoped token' do
+        provider.instance_variable_get('@property_hash')[:id] = '1cb05cfed7c24279be884ba4f6520262'
+        provider.instance_variable_get('@property_hash')[:domain] = 'foo_domain'
+        mockcreds = {}
+        Puppet::Provider::Openstack::CredentialsV3.expects(:new).returns(mockcreds)
+        mockcreds.expects(:auth_url=).with('http://127.0.0.1:5000')
+        mockcreds.expects(:password=).with('foo')
+        mockcreds.expects(:username=).with('foo')
+        mockcreds.expects(:user_id=).with('1cb05cfed7c24279be884ba4f6520262')
+        mockcreds.expects(:domain_name=).with('foo_domain')
+        mockcreds.expects(:to_env).returns(mockcreds)
+        Puppet::Provider::Openstack.expects(:openstack)
+                      .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                      .returns('"ID","Name","Domain ID","Description","Enabled"
+')
+        Puppet::Provider::Openstack.expects(:openstack)
+                      .with('token', 'issue', ['--format', 'value'])
+                      .returns('2015-05-14T04:06:05Z
+e664a386befa4a30878dcef20e79f167
+8dce2ae9ecd34c199d2877bf319a3d06
+ac43ec53d5a74a0b9f51523ae41a29f0
+')
+        password = provider.password
+        expect(password).to eq('foo')
+      end
     end
+  end
+
+  describe 'when updating a user with unmanaged password', :nohooks => true do
 
     describe 'when updating a user with unmanaged password' do
 
@@ -265,13 +388,239 @@ ac43ec53d5a74a0b9f51523ae41a29f0
           :replace_password => 'False',
           :tenant           => 'foo',
           :email            => 'foo@example.com',
+          :domain           => 'foo_domain',
         }
       end
 
+      let(:resource) do
+        Puppet::Type::Keystone_user.new(user_attrs)
+      end
+
+      let :provider do
+        provider_class.new(resource)
+      end
+
       it 'should not try to check password' do
         expect(provider.password).to eq('foo')
       end
     end
+  end
 
+  it_behaves_like 'authenticated with environment variables' do
+    describe 'v3 domains with no domain in resource', :nohooks => true do
+      let(:user_attrs) do
+        {
+          :name         => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :password     => 'foo',
+          :tenant       => 'foo',
+          :email        => 'foo@example.com',
+        }
+      end
+
+      it 'adds default domain to commands' do
+        provider_class.class_exec {
+          @default_domain_id = nil
+        }
+        mock = {
+          'identity' => {'default_domain_id' => 'foo_domain_id'}
+        }
+        Puppet::Util::IniConfig::File.expects(:new).returns(mock)
+        File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
+        mock.expects(:read).with('/etc/keystone/keystone.conf')
+        provider.class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', ['--user', '1cb05cfed7c24279be884ba4f6520262', '--long'])
+                     .returns('"ID","Name"
+')
+        project_class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                     .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'show', '--format', 'shell', '_member_')
+                      .returns('
+name="_member_"
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'add', ['_member_', '--project', '1cb05cfed7c24279be884ba4f6520262', '--user', '1cb05cfed7c24279be884ba4f6520262'])
+        provider.class.expects(:openstack)
+                      .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'foo_domain'])
+                    .returns('email="foo@example.com"
+enabled="True"
+id="1cb05cfed7c24279be884ba4f6520262"
+name="foo"
+username="foo"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("1cb05cfed7c24279be884ba4f6520262")
+      end
+    end
+
+    describe 'v3 domains with domain in resource' do
+      let(:user_attrs) do
+        {
+          :name         => 'foo',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :password     => 'foo',
+          :tenant       => 'foo',
+          :email        => 'foo@example.com',
+          :domain       => 'bar_domain',
+        }
+      end
+
+      it 'uses given domain in commands' do
+        project_class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                     .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'show', '--format', 'shell', '_member_')
+                      .returns('
+name="_member_"
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'add', ['_member_', '--project', '1cb05cfed7c24279be884ba4f6520262', '--user', '2cb05cfed7c24279be884ba4f6520262'])
+        provider.class.expects(:openstack)
+                      .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'bar_domain'])
+                      .returns('email="foo@example.com"
+enabled="True"
+id="2cb05cfed7c24279be884ba4f6520262"
+name="foo"
+username="foo"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("2cb05cfed7c24279be884ba4f6520262")
+      end
+    end
+
+    describe 'v3 domains with domain in name/title' do
+      let(:user_attrs) do
+        {
+          :name         => 'foo::bar_domain',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :password     => 'foo',
+          :tenant       => 'foo',
+          :email        => 'foo@example.com',
+        }
+      end
+
+      it 'uses given domain in commands' do
+        project_class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                     .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'show', '--format', 'shell', '_member_')
+                      .returns('
+name="_member_"
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'add', ['_member_', '--project', '1cb05cfed7c24279be884ba4f6520262', '--user', '2cb05cfed7c24279be884ba4f6520262'])
+        provider.class.expects(:openstack)
+                      .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'bar_domain'])
+                      .returns('email="foo@example.com"
+enabled="True"
+id="2cb05cfed7c24279be884ba4f6520262"
+name="foo"
+username="foo"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("2cb05cfed7c24279be884ba4f6520262")
+      end
+    end
+
+    describe 'v3 domains with domain in name/title and in resource' do
+      let(:user_attrs) do
+        {
+          :name         => 'foo::bar_domain',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :password     => 'foo',
+          :tenant       => 'foo',
+          :email        => 'foo@example.com',
+          :domain       => 'foo_domain',
+        }
+      end
+
+      it 'uses the resource domain in commands' do
+        project_class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                     .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","bar","bar_domain_id","bar",True
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'show', '--format', 'shell', '_member_')
+                      .returns('
+name="_member_"
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'add', ['_member_', '--project', '1cb05cfed7c24279be884ba4f6520262', '--user', '2cb05cfed7c24279be884ba4f6520262'])
+        provider.class.expects(:openstack)
+                      .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'foo_domain'])
+                      .returns('email="foo@example.com"
+enabled="True"
+id="2cb05cfed7c24279be884ba4f6520262"
+name="foo"
+username="foo"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("2cb05cfed7c24279be884ba4f6520262")
+      end
+    end
+
+    describe 'v3 domains with domain in name/title and in resource and in tenant' do
+      let(:user_attrs) do
+        {
+          :name         => 'foo::bar_domain',
+          :ensure       => 'present',
+          :enabled      => 'True',
+          :password     => 'foo',
+          :tenant       => 'foo::foo_domain',
+          :email        => 'foo@example.com',
+          :domain       => 'foo_domain',
+        }
+      end
+
+      it 'uses the resource domain in commands' do
+        project_class.expects(:openstack)
+                     .with('project', 'list', '--quiet', '--format', 'csv', '--long')
+                     .returns('"ID","Name","Domain ID","Description","Enabled"
+"1cb05cfed7c24279be884ba4f6520262","foo","foo_domain_id","foo",True
+"2cb05cfed7c24279be884ba4f6520262","foo","bar_domain_id","foo",True
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'show', '--format', 'shell', '_member_')
+                      .returns('
+name="_member_"
+')
+        provider.class.expects(:openstack)
+                      .with('role', 'add', ['_member_', '--project', '1cb05cfed7c24279be884ba4f6520262', '--user', '2cb05cfed7c24279be884ba4f6520262'])
+        provider.class.expects(:openstack)
+                      .with('user', 'create', '--format', 'shell', ['foo', '--enable', '--password', 'foo', '--email', 'foo@example.com', '--domain', 'foo_domain'])
+                      .returns('email="foo@example.com"
+enabled="True"
+id="2cb05cfed7c24279be884ba4f6520262"
+name="foo"
+username="foo"
+')
+        provider.create
+        expect(provider.exists?).to be_truthy
+        expect(provider.id).to eq("2cb05cfed7c24279be884ba4f6520262")
+      end
+    end
   end
 end
index 2490adc..7a88d85 100644 (file)
@@ -3,9 +3,183 @@ require 'spec_helper'
 require 'puppet/provider/keystone_user_role/openstack'
 
 provider_class = Puppet::Type.type(:keystone_user_role).provider(:openstack)
+def user_class
+  Puppet::Type.type(:keystone_user).provider(:openstack)
+end
+def project_class
+  Puppet::Type.type(:keystone_tenant).provider(:openstack)
+end
 
 describe provider_class do
 
+  # assumes Enabled is the last column - no quotes
+  def list_to_csv(thelist)
+    if thelist.is_a?(String)
+      return ''
+    end
+    str=""
+    thelist.each do |rec|
+      if rec.is_a?(String)
+        return ''
+      end
+      rec.each do |xx|
+        if xx.equal?(rec.last)
+          # True/False have no quotes
+          if xx == 'True' or xx == 'False'
+            str = str + xx + "\n"
+          else
+            str = str + '"' + xx + '"' + "\n"
+          end
+        else
+          str = str + '"' + xx + '",'
+        end
+      end
+    end
+    str
+  end
+
+  def before_need_instances
+    provider.class.expects(:openstack).once
+      .with('domain', 'list', '--quiet', '--format', 'csv')
+      .returns('"ID","Name","Enabled","Description"
+"foo_domain_id","foo_domain",True,"foo domain"
+"bar_domain_id","bar_domain",True,"bar domain"
+"another_domain_id","another_domain",True,"another domain"
+"disabled_domain_id","disabled_domain",False,"disabled domain"
+')
+    project_list = [['project-id-1','foo','foo_domain_id','foo project in foo domain','True'],
+                    ['project-id-2','foo','bar_domain_id','foo project in bar domain','True'],
+                    ['project-id-3','bar','foo_domain_id','bar project in foo domain','True'],
+                    ['project-id-4','etc','another_domain_id','another project','True']]
+
+    user_list_for_project = {
+      'project-id-1' => [['user-id-1','foo@example.com','foo','foo_domain','foo user','foo@foo_domain','True'],
+                         ['user-id-2','bar@example.com','foo','foo_domain','bar user','bar@foo_domain','True']],
+      'project-id-2' => [['user-id-3','foo@bar.com','foo','bar_domain','foo user','foo@bar_domain','True'],
+                         ['user-id-4','bar@bar.com','foo','bar_domain','bar user','bar@bar_domain','True']]
+    }
+    user_list_for_project.default = ''
+
+    user_list_for_domain = {
+      'foo_domain_id' => [['user-id-1','foo@example.com','foo','foo_domain','foo user','foo@foo_domain','True'],
+                          ['user-id-2','bar@example.com','foo','foo_domain','bar user','bar@foo_domain','True']],
+      'bar_domain_id' => [['user-id-3','foo@bar.com','foo','bar_domain','foo user','foo@bar_domain','True'],
+                          ['user-id-4','bar@bar.com','foo','bar_domain','bar user','bar@bar_domain','True']]
+    }
+    user_list_for_domain.default = ''
+
+    role_list_for_project_user = {
+      'project-id-1' => {
+        'user-id-1' => [['role-id-1','foo','foo','foo'],
+                        ['role-id-2','bar','foo','foo']]
+      },
+      'project-id-2' => {
+        'user-id-3' => [['role-id-1','foo','foo','foo'],
+                        ['role-id-2','bar','foo','foo']]
+      }
+    }
+    role_list_for_project_user.default = ''
+
+    role_list_for_domain_user = {
+      'foo_domain_id' => {
+        'user-id-2' => [['role-id-1','foo','foo_domain','foo'],
+                        ['role-id-2','bar','foo_domain','foo']]
+      },
+      'bar_domain_id' => {
+        'user-id-4' => [['role-id-1','foo','bar_domain','foo'],
+                        ['role-id-2','bar','bar_domain','foo']]
+      }
+    }
+    role_list_for_project_user.default = ''
+
+    provider.class.expects(:openstack).once
+                  .with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
+                  .returns('"ID","Name","Domain ID","Description","Enabled"' + "\n" + list_to_csv(project_list))
+    project_list.each do |rec|
+      csvlist = list_to_csv(user_list_for_project[rec[0]])
+      provider.class.expects(:openstack)
+                    .with('user', 'list', '--quiet', '--format', 'csv', ['--long', '--project', rec[0]])
+                    .returns('"ID","Name","Project","Domain","Description","Email","Enabled"' + "\n" + csvlist)
+      next if csvlist == ''
+      user_list_for_project[rec[0]].each do |urec|
+        csvlist = ''
+        if role_list_for_project_user.has_key?(rec[0]) and
+            role_list_for_project_user[rec[0]].has_key?(urec[0])
+          csvlist = list_to_csv(role_list_for_project_user[rec[0]][urec[0]])
+        end
+        provider.class.expects(:openstack)
+                      .with('role', 'list', '--quiet', '--format', 'csv', ['--project', rec[0], '--user', urec[0]])
+                      .returns('"ID","Name","Project","User"' + "\n" + csvlist)
+      end
+    end
+    ['foo_domain_id', 'bar_domain_id'].each do |domid|
+      csvlist = list_to_csv(user_list_for_domain[domid])
+      provider.class.expects(:openstack)
+                    .with('user', 'list', '--quiet', '--format', 'csv', ['--long', '--domain', domid])
+                    .returns('"ID","Name","Project","Domain","Description","Email","Enabled"' + "\n" + csvlist)
+      next if csvlist == ''
+      user_list_for_domain[domid].each do |urec|
+        csvlist = ''
+        if role_list_for_domain_user.has_key?(domid) and
+            role_list_for_domain_user[domid].has_key?(urec[0])
+          csvlist = list_to_csv(role_list_for_domain_user[domid][urec[0]])
+        end
+        provider.class.expects(:openstack)
+                      .with('role', 'list', '--quiet', '--format', 'csv', ['--domain', domid, '--user', urec[0]])
+                      .returns('"ID","Name","Domain","User"' + "\n" + csvlist)
+      end
+    end
+  end
+
+  def before_common(destroy, nolist=false, instances=false)
+    rolelistprojectuser = [['role-id-1','foo','foo','foo'],
+                           ['role-id-2','bar','foo','foo']]
+    csvlist = list_to_csv(rolelistprojectuser)
+    rolelistreturns = ['"ID","Name","Project","User"' + "\n" + csvlist]
+    nn = 1
+    if destroy
+      rolelistreturns = ['']
+      nn = 1
+    end
+    unless nolist
+      provider.class.expects(:openstack).times(nn)
+                    .with('role', 'list', '--quiet', '--format', 'csv', ['--project', 'project-id-1', '--user', 'user-id-1'])
+                    .returns(*rolelistreturns)
+    end
+
+    userhash = {:id => 'user-id-1', :name => 'foo@example.com'}
+    usermock = user_class.new(userhash)
+    unless instances
+      usermock.expects(:exists?).with(any_parameters).returns(true)
+      user_class.expects(:new).twice.with(any_parameters).returns(usermock)
+    end
+    user_class.expects(:instances).with(any_parameters).returns([usermock])
+
+    projecthash = {:id => 'project-id-1', :name => 'foo'}
+    projectmock = project_class.new(projecthash)
+    unless instances
+      projectmock.expects(:exists?).with(any_parameters).returns(true)
+      project_class.expects(:new).with(any_parameters).returns(projectmock)
+    end
+    project_class.expects(:instances).with(any_parameters).returns([projectmock])
+  end
+
+  before :each, :default => true do
+    before_common(false)
+  end
+
+  before :each, :destroy => true do
+    before_common(true)
+  end
+
+  before :each, :nolist => true do
+    before_common(true, true)
+  end
+
+  before :each, :instances => true do
+    before_common(true, true, true)
+  end
+
   shared_examples 'authenticated with environment variables' do
     ENV['OS_USERNAME']     = 'test'
     ENV['OS_PASSWORD']     = 'abc123'
@@ -31,59 +205,83 @@ describe provider_class do
         provider_class.new(resource)
       end
 
-      before(:each) do
-        provider.class.stubs(:openstack)
-                      .with('user', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                      .returns('"ID","Name","Project","User"
-"1cb05cfed7c24279be884ba4f6520262","foo","foo","foo"
-')
-      end
-
-      describe '#create' do
+      describe '#create', :default => true do
         it 'adds all the roles to the user' do
-          provider.class.stubs(:openstack)
-                        .with('role', 'add', ['foo', '--project', 'foo', '--user', 'foo'])
-          provider.class.stubs(:openstack)
-                        .with('role', 'add', ['bar', '--project', 'foo', '--user', 'foo'])
-          provider.class.stubs(:openstack)
-                        .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                        .returns('"ID","Name","Project","User"
-"1cb05ed7c24279be884ba4f6520262","foo","foo","foo"
-"2cb05ed7c24279be884ba4f6520262","bar","foo","foo"
-')
+          provider.class.expects(:openstack)
+                        .with('role', 'add', ['foo', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.class.expects(:openstack)
+                        .with('role', 'add', ['bar', '--project', 'project-id-1', '--user', 'user-id-1'])
           provider.create
           expect(provider.exists?).to be_truthy
         end
       end
 
-      describe '#destroy' do
+      describe '#destroy', :destroy => true do
         it 'removes all the roles from a user' do
-          provider.class.stubs(:openstack)
-                        .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                        .returns('"ID","Name","Project","User"')
-          provider.class.stubs(:openstack)
-                        .with('role', 'remove', ['foo', '--project', 'foo', '--user', 'foo'])
-          provider.class.stubs(:openstack)
-                        .with('role', 'remove', ['bar', '--project', 'foo', '--user', 'foo'])
+          provider.instance_variable_get('@property_hash')[:roles] = ['foo', 'bar']
+          provider.class.expects(:openstack)
+                        .with('role', 'remove', ['foo', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.class.expects(:openstack)
+                        .with('role', 'remove', ['bar', '--project', 'project-id-1', '--user', 'user-id-1'])
           provider.destroy
           expect(provider.exists?).to be_falsey
         end
 
       end
 
-      describe '#exists' do
+      describe '#exists', :default => true do
         subject(:response) do
-          provider.class.stubs(:openstack)
-                        .with('user role', 'list', '--quiet', '--format', 'csv', ['foo', '--project', 'foo'])
-                        .returns('"ID","Name","Project","User"
-"1cb05ed7c24279be884ba4f6520262","foo","foo","foo"
-')
           response = provider.exists?
         end
 
         it { is_expected.to be_truthy }
 
       end
+
+      describe '#instances', :instances => true do
+        it 'finds every user role' do
+          provider.class.expects(:openstack)
+                        .with('role', 'list', '--quiet', '--format', 'csv', [])
+                        .returns('"ID","Name"
+"foo-role-id","foo"
+"bar-role-id","bar"
+')
+          provider.class.expects(:openstack)
+                        .with('role assignment', 'list', '--quiet', '--format', 'csv', [])
+                        .returns('
+"Role","User","Group","Project","Domain"
+"foo-role-id","user-id-1","","project-id-1",""
+"bar-role-id","user-id-1","","project-id-1",""
+')
+          instances = provider.class.instances
+          expect(instances.count).to eq(1)
+          expect(instances[0].name).to eq('foo@example.com@foo')
+          expect(instances[0].roles).to eq(['foo', 'bar'])
+        end
+      end
+
+      describe '#roles=', :nolist => true do
+        let(:user_role_attrs) do
+          {
+            :name         => 'foo@foo',
+            :ensure       => 'present',
+            :roles        => ['one', 'two'],
+          }
+        end
+
+        it 'applies the new roles' do
+          provider.instance_variable_get('@property_hash')[:roles] = ['foo', 'bar']
+          provider.class.expects(:openstack)
+                        .with('role', 'remove', ['foo', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.class.expects(:openstack)
+                        .with('role', 'remove', ['bar', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.class.expects(:openstack)
+                        .with('role', 'add', ['one', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.class.expects(:openstack)
+                        .with('role', 'add', ['two', '--project', 'project-id-1', '--user', 'user-id-1'])
+          provider.roles=(['one', 'two'])
+        end
+      end
     end
   end
 end
diff --git a/3rdparty/modules/keystone/spec/unit/type/keystone_tenant_spec.rb b/3rdparty/modules/keystone/spec/unit/type/keystone_tenant_spec.rb
new file mode 100644 (file)
index 0000000..978fa22
--- /dev/null
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require 'puppet'
+require 'puppet/type/keystone_tenant'
+
+describe Puppet::Type.type(:keystone_tenant) do
+
+  before :each do
+    @project = Puppet::Type.type(:keystone_tenant).new(
+    :name   => 'foo',
+    :domain => 'foo-domain',
+    )
+
+    @domain = @project.parameter('domain')
+  end
+
+  it 'should not be in sync for domain changes' do
+    expect { @domain.insync?('not-the-domain') }.to raise_error(Puppet::Error, /The domain cannot be changed from/)
+    expect { @domain.insync?(nil) }.to raise_error(Puppet::Error, /The domain cannot be changed from/)
+  end
+
+  it 'should be in sync if domain is the same' do
+    expect(@domain.insync?('foo-domain')).to be true
+  end
+
+end
diff --git a/3rdparty/modules/keystone/spec/unit/type/keystone_user_spec.rb b/3rdparty/modules/keystone/spec/unit/type/keystone_user_spec.rb
new file mode 100644 (file)
index 0000000..789af43
--- /dev/null
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require 'puppet'
+require 'puppet/type/keystone_user'
+
+describe Puppet::Type.type(:keystone_user) do
+
+  before :each do
+    @project = Puppet::Type.type(:keystone_user).new(
+    :name   => 'foo',
+    :domain => 'foo-domain',
+    )
+
+    @domain = @project.parameter('domain')
+  end
+
+  it 'should not be in sync for domain changes' do
+    expect { @domain.insync?('not-the-domain') }.to raise_error(Puppet::Error, /The domain cannot be changed from/)
+    expect { @domain.insync?(nil) }.to raise_error(Puppet::Error, /The domain cannot be changed from/)
+  end
+
+  it 'should be in sync if domain is the same' do
+    expect(@domain.insync?('foo-domain')).to be true
+  end
+
+end