From 68f359c568f9f8a74b9039c3ba7199141b23a439 Mon Sep 17 00:00:00 2001 From: mattray Date: Wed, 20 Jun 2012 16:56:53 -0500 Subject: [PATCH] initial pull from rcbops-cookbooks/keystone, monitoring removed --- .gitignore | 1 + attributes/default.rb | 83 ++++ files/default/keystone_plugin.py | 79 ++++ providers/credentials.rb | 128 ++++++ providers/register.rb | 445 ++++++++++++++++++++ recipes/server.rb | 232 ++++++++++ resources/credentials.rb | 17 + resources/register.rb | 44 ++ templates/default/keystone-logging.conf.erb | 49 +++ templates/default/keystone.conf.erb | 107 +++++ 10 files changed, 1185 insertions(+) create mode 100644 .gitignore create mode 100644 attributes/default.rb create mode 100644 files/default/keystone_plugin.py create mode 100644 providers/credentials.rb create mode 100644 providers/register.rb create mode 100644 recipes/server.rb create mode 100644 resources/credentials.rb create mode 100644 resources/register.rb create mode 100644 templates/default/keystone-logging.conf.erb create mode 100644 templates/default/keystone.conf.erb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54e2457 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +metadata.json diff --git a/attributes/default.rb b/attributes/default.rb new file mode 100644 index 0000000..d7f954d --- /dev/null +++ b/attributes/default.rb @@ -0,0 +1,83 @@ +######################################################################## +# Toggles - These can be overridden at the environment level +default["enable_monit"] = false # OS provides packages +default["developer_mode"] = false # we want secure passwords by default +######################################################################## + +# Adding these as blank +# this needs to be here for the initial deep-merge to work +default["credentials"]["EC2"]["admin"]["access"] = "" +default["credentials"]["EC2"]["admin"]["secret"] = "" + +default["keystone"]["db"]["name"] = "keystone" +default["keystone"]["db"]["username"] = "keystone" +# Replacing with OpenSSL::Password in recipes/server.rb +# default["keystone"]["db"]["password"] = "keystone" + +default["keystone"]["verbose"] = "False" +default["keystone"]["debug"] = "False" + +# new endpoint location stuff +default["keystone"]["services"]["admin-api"]["scheme"] = "http" +default["keystone"]["services"]["admin-api"]["network"] = "nova" +default["keystone"]["services"]["admin-api"]["port"] = "35357" +default["keystone"]["services"]["admin-api"]["path"] = "/v2.0" + +default["keystone"]["services"]["service-api"]["scheme"] = "http" +default["keystone"]["services"]["service-api"]["network"] = "public" +default["keystone"]["services"]["service-api"]["port"] = "5000" +default["keystone"]["services"]["service-api"]["path"] = "/v2.0" + + +# default["keystone"]["roles"] = [ "admin", "Member", "KeystoneAdmin", "KeystoneServiceAdmin", "sysadmin", "netadmin" ] +default["keystone"]["roles"] = [ "admin", "Member", "KeystoneAdmin", "KeystoneServiceAdmin" ] + +default["keystone"]["tenants"] = [ "admin", "demo", "service"] + +default["keystone"]["admin_user"] = "admin" + +default["keystone"]["users"] = { + default["keystone"]["admin_user"] => { + "password" => "secrete", + "default_tenant" => "admin", + "roles" => { + "admin" => [ "admin", "demo" ], + "KeystoneAdmin" => [ "admin" ], + "KeystoneServiceAdmin" => [ "admin" ] + } + }, + "demo" => { + "password" => "secrete", + "default_tenant" => "demo", + "roles" => { + "Member" => [ "demo" ] + } + }, + "monitoring" => { + "password" => "secrete", + "default_tenant" => "service", + "roles" => { + "Member" => [ "demo", "admin" ] + } + } +} + + +# platform defaults +case platform +when "fedora" + default["keystone"]["platform"] = { + "mysql_python_packages" => [ "MySQL-python" ], + "keystone_packages" => [ "openstack-keystone" ], + "keystone_service" => "openstack-keystone", + "package_options" => "" + } +when "ubuntu" + default["keystone"]["platform"] = { + "mysql_python_packages" => [ "python-mysqldb" ], + "keystone_packages" => [ "keystone" ], + "keystone_service" => "keystone", + "package_options" => "-o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef'" + } +end + diff --git a/files/default/keystone_plugin.py b/files/default/keystone_plugin.py new file mode 100644 index 0000000..950a756 --- /dev/null +++ b/files/default/keystone_plugin.py @@ -0,0 +1,79 @@ +from keystoneclient.v2_0 import Client as KeystoneClient + +import collectd + +global OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_AUTH_URL, VERBOSE_LOGGING + +OS_USERNAME = "username" +OS_PASSWORD = "password" +OS_TENANT_NAME = "tenantname" +OS_AUTH_URL = "http://localhost:5000/v2.0" +VERBOSE_LOGGING = False + + +def get_stats(user, passwd, tenant, url): + keystone = KeystoneClient(username=user, password=passwd, tenant_name=tenant, auth_url=url) + data = dict() + + # Define list of keys to query for + keys = ('tenants','users','roles','services','endpoints') + for key in keys: + data["openstack.keystone.%s.count" % key] = len(keystone.__getattribute__(key).list()) + + tenant_list = keystone.tenants.list() + for tenant in tenant_list: + data["openstack.keystone.tenants.tenants.%s.users.count" % tenant.name] = len(keystone.tenants.list_users(tenant.id)) + + ########## + # debug + #for key in data.keys(): + # print "%s = %s" % (key, data[key]) + ########## + + return data + +def configure_callback(conf): + """Received configuration information""" + global OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_AUTH_URL, VERBOSE_LOGGING + for node in conf.children: + if node.key == "Username": + OS_USERNAME = node.values[0] + elif node.key == "Password": + OS_PASSWORD = node.values[0] + elif node.key == "TenantName": + OS_TENANT_NAME = node.values[0] + elif node.key == "AuthURL": + OS_AUTH_URL = node.values[0] + elif node.key == "Verbose": + VERBOSE_LOGGING = node.values[0] + else: + logger("warn", "Unknown config key: %s" % node.key) + + +def read_callback(): + logger("verb", "read_callback") + info = get_stats(OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_AUTH_URL) + + if not info: + logger("err", "No information received") + return + + for key in info.keys(): + logger('verb', 'Dispatching %s : %i' % (key, int(info[key]))) + val = collectd.Values(plugin=key) + val.type = 'gauge' + val.values = [int(info[key])] + val.dispatch() + + +def logger(t, msg): + if t == 'err': + collectd.error('%s: %s' % (NAME, msg)) + if t == 'warn': + collectd.warning('%s: %s' % (NAME, msg)) + elif t == 'verb' and VERBOSE_LOGGING == True: + collectd.info('%s: %s' % (NAME, msg)) + +collectd.register_config(configure_callback) +collectd.warning("Initializing keystone plugin") +collectd.register_read(read_callback) diff --git a/providers/credentials.rb b/providers/credentials.rb new file mode 100644 index 0000000..8b67a0b --- /dev/null +++ b/providers/credentials.rb @@ -0,0 +1,128 @@ + +action :create_ec2 do + Chef::Log.debug("Keystone/Providers/Credentials.rb") + Chef::Log.debug("Create_EC2 actions") + + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # lookup tenant_uuid + Chef::Log.debug("Looking up Tenant_UUID for Tenant_Name: #{new_resource.tenant_name}") + tenant_container = "tenants" + tenant_key = "name" + tenant_path = "/#{new_resource.api_ver}/tenants" + tenant_uuid, tenant_error = _find_id(http, tenant_path, headers, tenant_container, tenant_key, new_resource.tenant_name) + Chef::Log.error("There was an error looking up Tenant '#{new_resource.tenant_name}'") if tenant_error + + # lookup user_uuid + Chef::Log.debug("Looking up User_UUID for User_Name: #{new_resource.user_name}") + user_container = "users" + user_key = "name" + user_path = "/#{new_resource.api_ver}/tenants/#{tenant_uuid}/users" + user_uuid, user_error = _find_id(http, user_path, headers, user_container, user_key, new_resource.user_name) + Chef::Log.error("There was an error looking up User '#{new_resource.user_name}'") if user_error + + Chef::Log.debug("Found Tenant UUID: #{tenant_uuid}") + Chef::Log.debug("Found User UUID: #{user_uuid}") + + # See if a set of credentials already exists for this user/tenant combo + cred_container = "credentials" + cred_key = "tenant_id" + cred_path = "/#{new_resource.api_ver}/users/#{user_uuid}/credentials/OS-EC2" + cred_tenant_uuid, cred_error = _find_cred_id(http, cred_path, headers, cred_container, cred_key, tenant_uuid) + Chef::Log.error("There was an error looking up EC2 Credentials for User '#{new_resource.user_name}' and Tenant '#{new_resource.tenant_name}'") if cred_error + + error = (tenant_error or user_error or cred_error) + unless cred_tenant_uuid or error + # Construct the extension path + path = "/#{new_resource.api_ver}/users/#{user_uuid}/credentials/OS-EC2" + + payload = _build_credentials_obj(tenant_uuid) + + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created EC2 Credentials for User '#{new_resource.user_name}' in Tenant '#{new_resource.tenant_name}'") + # Need to parse the output here and update node attributes + data = JSON.parse(resp.body) + node.set['credentials']['EC2'][new_resource.user_name]['access'] = data['credential']['access'] + node.set['credentials']['EC2'][new_resource.user_name]['secret'] = data['credential']['secret'] + node.save unless Chef::Config[:solo] + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create EC2 Credentials for User '#{new_resource.user_name}' in Tenant '#{new_resource.tenant_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + else + Chef::Log.info("Credentials already exist for User '#{new_resource.user_name}' in Tenant '#{new_resource.tenant_name}'.. Not creating.") + new_resource.updated_by_last_action(false) + end +end + + +private +def _find_id(http, path, headers, container, key, match_value) + uuid = nil + error = false + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + data = JSON.parse(resp.body) + data[container].each do |obj| + uuid = obj['id'] if obj[key] == match_value + break if uuid + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + error = true + end + return uuid,error +end + + +private +def _find_cred_id(http, path, headers, container, key, match_value) + uuid = nil + error = false + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + data = JSON.parse(resp.body) + data[container].each do |obj| + uuid = obj['tenant_id'] if obj[key] == match_value + break if uuid + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + error = true + end + return uuid,error +end + +private +def _build_credentials_obj(tenant_uuid) + ret = Hash.new + ret.store("tenant_id", tenant_uuid) + return ret +end + + +private +def _build_headers(token) + ret = Hash.new + ret.store('X-Auth-Token', token) + ret.store('Content-type', 'application/json') + ret.store('user-agent', 'Chef keystone_credentials') + return ret +end diff --git a/providers/register.rb b/providers/register.rb new file mode 100644 index 0000000..29070cd --- /dev/null +++ b/providers/register.rb @@ -0,0 +1,445 @@ + +action :create_service do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # Construct the extension path + path = "/#{new_resource.api_ver}/OS-KSADM/services" + + # See if the service exists yet + service_uuid, error = _find_service_id(http, path, headers, new_resource.service_type) + unless service_uuid + # Service does not exist yet + payload = _build_service_object(new_resource.service_type, new_resource.service_name, new_resource.service_description) + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created service '#{new_resource.service_name}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create service '#{new_resource.service_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + else + Chef::Log.info("Service Type '#{new_resource.service_type}' already exists.. Not creating.") if error + Chef::Log.info("Service UUID: #{service_uuid}") + new_resource.updated_by_last_action(false) + end +end + + +action :create_endpoint do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # Construct the extension path + path = "/#{new_resource.api_ver}/endpoints" + + # Lookup the service_uuid for service_type + service_uuid, error = _find_service_id(http, "/#{new_resource.api_ver}/OS-KSADM/services", headers, new_resource.service_type) + unless service_uuid + Chef::Log.error("Unable to find service type '#{new_resource.service_type}'") + new_resource.updated_by_last_action(false) + return + end + + # Make sure this endpoint does not already exist + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + endpoint_exists = false + data = JSON.parse(resp.body) + data['endpoints'].each do |endpoint| + if endpoint['service_id'] == service_uuid + # Match found + endpoint_exists = true + break + end + end + if endpoint_exists + Chef::Log.info("Endpoint already exists for Service Type '#{new_resource.service_type}' already exists.. Not creating.") + new_resource.updated_by_last_action(false) + else + payload = _build_endpoint_object( + new_resource.endpoint_region, + service_uuid, + new_resource.endpoint_publicurl, + new_resource.endpoint_internalurl, + new_resource.endpoint_adminurl) + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created endpoint for service type '#{new_resource.service_type}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create endpoint for service type '#{new_resource.service_type}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end +end + +action :create_tenant do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # Construct the extension path + path = "/#{new_resource.api_ver}/tenants" + + # See if the service exists yet + tenant_uuid, error = _find_tenant_id(http, path, headers, new_resource.tenant_name) + unless tenant_uuid + # Service does not exist yet + payload = _build_tenant_object(new_resource.tenant_name, new_resource.service_description, new_resource.tenant_enabled) + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created tenant '#{new_resource.tenant_name}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create tenant '#{new_resource.tenant_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + else + Chef::Log.info("Tenant '#{new_resource.tenant_name}' already exists.. Not creating.") if error + Chef::Log.info("Tenant UUID: #{tenant_uuid}") + new_resource.updated_by_last_action(false) + end +end + +action :create_role do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # Construct the extension path + path = "/#{new_resource.api_ver}/OS-KSADM/roles" + + container = "roles" + key = "name" + + # See if the role exists yet + role_uuid, error = _find_id(http, path, headers, container, key, new_resource.role_name) + unless role_uuid + # role does not exist yet + payload = _build_role_obj(new_resource.role_name) + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created Role '#{new_resource.role_name}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create role '#{new_resource.role_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + else + Chef::Log.info("Role '#{new_resource.role_name}' already exists.. Not creating.") if error + Chef::Log.info("Role UUID: #{role_uuid}") + new_resource.updated_by_last_action(false) + end +end + +action :create_user do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # Lookup the tenant_uuid for tenant_name + tenant_uuid, error = _find_tenant_id(http, "/#{new_resource.api_ver}/tenants", headers, new_resource.tenant_name) + unless tenant_uuid + Chef::Log.error("Unable to find tenant '#{new_resource.tenant_name}'") + new_resource.updated_by_last_action(false) + return + end + + # Construct the extension path using the found tenant_uuid + path = "/#{new_resource.api_ver}/users" + + # Make sure this endpoint does not already exist + resp = http.request_get("#{new_resource.api_ver}/tenants/#{tenant_uuid}/users", headers) + if resp.is_a?(Net::HTTPOK) + user_exists = false + data = JSON.parse(resp.body) + data['users'].each do |endpoint| + if endpoint['name'] == new_resource.user_name + # Match found + user_exists = true + break + end + end + if user_exists + Chef::Log.info("User '#{new_resource.user_name}' already exists for tenant '#{new_resource.tenant_name}'") + new_resource.updated_by_last_action(false) + else + payload = _build_user_object( + tenant_uuid, + new_resource.user_name, + new_resource.user_pass, + new_resource.user_enabled) + resp = http.send_request('POST', path, JSON.generate(payload), headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Created user '#{new_resource.user_name}' for tenant '#{new_resource.tenant_name}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to create user '#{new_resource.user_name}' for tenant '#{new_resource.tenant_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end +end + +action :grant_role do + # construct a HTTP object + http = Net::HTTP.new(new_resource.auth_host, new_resource.auth_port) + + # Check to see if connection is http or https + if new_resource.auth_protocol == "https" + http.use_ssl = true + end + + # Build out the required header info + headers = _build_headers(new_resource.auth_token) + + # lookup tenant_uuid + tenant_container = "tenants" + tenant_key = "name" + tenant_path = "/#{new_resource.api_ver}/tenants" + tenant_uuid, tenant_error = _find_id(http, tenant_path, headers, tenant_container, tenant_key, new_resource.tenant_name) + Chef::Log.error("There was an error looking up Tenant '#{new_resource.tenant_name}'") if tenant_error + + # lookup user_uuid + user_container = "users" + user_key = "name" + # user_path = "/#{new_resource.api_ver}/tenants/#{tenant_uuid}/users" + user_path = "/#{new_resource.api_ver}/users" + user_uuid, user_error = _find_id(http, user_path, headers, user_container, user_key, new_resource.user_name) + Chef::Log.error("There was an error looking up User '#{new_resource.user_name}'") if user_error + + # lookup role_uuid + role_container = "roles" + role_key = "name" + role_path = "/#{new_resource.api_ver}/OS-KSADM/roles" + role_uuid, role_error = _find_id(http, role_path, headers, role_container, role_key, new_resource.role_name) + Chef::Log.error("There was an error looking up Role '#{new_resource.role_name}'") if role_error + + Chef::Log.debug("Found Tenant UUID: #{tenant_uuid}") + Chef::Log.debug("Found User UUID: #{user_uuid}") + Chef::Log.debug("Found Role UUID: #{role_uuid}") + + # lookup roles assigned to user/tenant + assigned_container = "roles" + assigned_key = "name" + assigned_path = "/#{new_resource.api_ver}/tenants/#{tenant_uuid}/users/#{user_uuid}/roles" + assigned_role_uuid, assigned_error = _find_id(http, assigned_path, headers, assigned_container, assigned_key, new_resource.role_name) + Chef::Log.error("There was an error looking up Assigned Role '#{new_resource.role_name}' for User '#{new_resource.user_name}' and Tenant '#{new_resource.tenant_name}'") if assigned_error + + error = (tenant_error or user_error or role_error or assigned_error) + unless role_uuid == assigned_role_uuid or error + # Construct the extension path + path = "/#{new_resource.api_ver}/tenants/#{tenant_uuid}/users/#{user_uuid}/roles/OS-KSADM/#{role_uuid}" + + # needs a '' for the body, or it throws a 500 + resp = http.send_request('PUT', path, '', headers) + if resp.is_a?(Net::HTTPOK) + Chef::Log.info("Granted Role '#{new_resource.role_name}' to User '#{new_resource.user_name}' in Tenant '#{new_resource.tenant_name}'") + new_resource.updated_by_last_action(true) + else + Chef::Log.error("Unable to grant role '#{new_resource.role_name}'") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + new_resource.updated_by_last_action(false) + end + else + Chef::Log.info("Role '#{new_resource.role_name}' already exists.. Not granting.") + Chef::Log.error("There was an error looking up '#{new_resource.role_name}'") if error + new_resource.updated_by_last_action(false) + end +end + +private +def _find_service_id(http, path, headers, service_type) + service_uuid = nil + error = false + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + data = JSON.parse(resp.body) + data['OS-KSADM:services'].each do |svc| + service_uuid = svc['id'] if svc['type'] == service_type + break if service_uuid + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + error = true + end + return service_uuid,error +end + + +private +def _find_id(http, path, headers, container, key, match_value) + uuid = nil + error = false + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + data = JSON.parse(resp.body) + data[container].each do |obj| + uuid = obj['id'] if obj[key] == match_value + break if uuid + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + error = true + end + return uuid,error +end + + +private +def _find_tenant_id(http, path, headers, tenant_name) + tenant_uuid = nil + error = false + resp = http.request_get(path, headers) + if resp.is_a?(Net::HTTPOK) + data = JSON.parse(resp.body) + data['tenants'].each do |tenant| + tenant_uuid = tenant['id'] if tenant['name'] == tenant_name + break if tenant_uuid + end + else + Chef::Log.error("Unknown response from the Keystone Server") + Chef::Log.error("Response Code: #{resp.code}") + Chef::Log.error("Response Message: #{resp.message}") + error = true + end + return tenant_uuid,error +end + + +private +def _build_service_object(type, name, description) + service_obj = Hash.new + service_obj.store("type", type) + service_obj.store("name", name) + service_obj.store("description", description) + ret = Hash.new + ret.store("OS-KSADM:service", service_obj) + return ret +end + + +private +def _build_role_obj(name) + role_obj = Hash.new + role_obj.store("name", name) + ret = Hash.new + ret.store("role", role_obj) + return ret +end + + +private +def _build_tenant_object(name, description, enabled) + tenant_obj = Hash.new + tenant_obj.store("name", name) + tenant_obj.store("description", description) + tenant_obj.store("enabled", enabled) + ret = Hash.new + ret.store("tenant", tenant_obj) + return ret +end + + +private +def _build_endpoint_object(region, service_uuid, publicurl, internalurl, adminurl) + endpoint_obj = Hash.new + endpoint_obj.store("adminurl", adminurl) + endpoint_obj.store("internalurl", internalurl) + endpoint_obj.store("publicurl", publicurl) + endpoint_obj.store("service_id", service_uuid) + endpoint_obj.store("region", region) + ret = Hash.new + ret.store("endpoint", endpoint_obj) + return ret +end + + +private +def _build_user_object(tenant_uuid, name, password, enabled) + user_obj = Hash.new + user_obj.store("tenantId", tenant_uuid) + user_obj.store("name", name) + user_obj.store("password", password) + # Have to provide a null value for this because I dont want to have this in the LWRP + user_obj.store("email", nil) + user_obj.store("enabled", enabled) + ret = Hash.new + ret.store("user", user_obj) + return ret +end + + +private +def _build_headers(token) + ret = Hash.new + ret.store('X-Auth-Token', token) + ret.store('Content-type', 'application/json') + ret.store('user-agent', 'Chef keystone_register') + return ret +end diff --git a/recipes/server.rb b/recipes/server.rb new file mode 100644 index 0000000..f5c3752 --- /dev/null +++ b/recipes/server.rb @@ -0,0 +1,232 @@ +# +# Cookbook Name:: keystone +# Recipe:: server +# +# Copyright 2009, Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +::Chef::Recipe.send(:include, Opscode::OpenSSL::Password) +include_recipe "mysql::client" +include_recipe "osops-utils" + +# Allow for using a well known db password +if node["developer_mode"] + node.set_unless["keystone"]["db"]["password"] = "keystone" + node.set_unless["keystone"]["admin_token"] = "999888777666" +else + node.set_unless["keystone"]["db"]["password"] = secure_password + node.set_unless["keystone"]["admin_token"] = secure_password +end + +platform_options = node["keystone"]["platform"] + +#creates db and user, returns connection info, defined in osops-utils/libraries +mysql_info = create_db_and_user("mysql", + node["keystone"]["db"]["name"], + node["keystone"]["db"]["username"], + node["keystone"]["db"]["password"]) + +##### NOTE ##### +# https://bugs.launchpad.net/ubuntu/+source/keystone/+bug/931236 +################ + +platform_options["mysql_python_packages"].each do |pkg| + package pkg do + action :install + end +end + +platform_options["keystone_packages"].each do |pkg| + package pkg do + action :upgrade + options platform_options["package_options"] + end +end + +execute "Keystone: sleep" do + command "sleep 10s" + action :nothing +end + +service "keystone" do + service_name platform_options["keystone_service"] + supports :status => true, :restart => true + action [ :enable ] + notifies :run, resources(:execute => "Keystone: sleep"), :immediately +end + +directory "/etc/keystone" do + action :create + owner "root" + group "root" + mode "0755" +end + +file "/var/lib/keystone/keystone.db" do + action :delete +end + +execute "keystone-manage db_sync" do + command "keystone-manage db_sync" + action :nothing +end + +ks_admin_endpoint = get_bind_endpoint("keystone", "admin-api") +ks_service_endpoint = get_bind_endpoint("keystone", "service-api") + + +template "/etc/keystone/keystone.conf" do + source "keystone.conf.erb" + owner "root" + group "root" + mode "0644" + variables( + :debug => node["keystone"]["debug"], + :verbose => node["keystone"]["verbose"], + :user => node["keystone"]["db"]["username"], + :passwd => node["keystone"]["db"]["password"], + :ip_address => ks_admin_endpoint["host"], + :db_name => node["keystone"]["db"]["name"], + :db_ipaddress => mysql_info["bind_address"], + :service_port => ks_service_endpoint["port"], + :admin_port => ks_admin_endpoint["port"], + :admin_token => node["keystone"]["admin_token"] + ) + notifies :run, resources(:execute => "keystone-manage db_sync"), :immediately + notifies :restart, resources(:service => "keystone"), :immediately +end + +template "/etc/keystone/logging.conf" do + source "keystone-logging.conf.erb" + owner "root" + group "root" + mode "0644" + notifies :restart, resources(:service => "keystone"), :immediately +end + +node["keystone"]["tenants"].each do |tenant_name| + ## Add openstack tenant ## + keystone_register "Register '#{tenant_name}' Tenant" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + tenant_name tenant_name + tenant_description "#{tenant_name} Tenant" + tenant_enabled "true" # Not required as this is the default + action :create_tenant + end +end + +## Add Roles ## +node["keystone"]["roles"].each do |role_key| + keystone_register "Register '#{role_key.to_s}' Role" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + role_name role_key + action :create_role + end +end + +node["keystone"]["users"].each do |username, user_info| + keystone_register "Register '#{username}' User" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + user_name username + user_pass user_info["password"] + tenant_name user_info["default_tenant"] + user_enabled "true" # Not required as this is the default + action :create_user + end + + user_info["roles"].each do |rolename, tenant_list| + tenant_list.each do |tenantname| + keystone_register "Grant '#{rolename}' Role to '#{username}' User in '#{tenantname}' Tenant" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + user_name username + role_name rolename + tenant_name tenantname + action :grant_role + end + end + + end +end + +## Add Services ## + +keystone_register "Register Identity Service" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + service_name "keystone" + service_type "identity" + service_description "Keystone Identity Service" + action :create_service +end + +## Add Endpoints ## + +node["keystone"]["adminURL"] = ks_admin_endpoint["uri"] +node["keystone"]["internalURL"] = ks_service_endpoint["uri"] +node["keystone"]["publicURL"] = ks_service_endpoint["uri"] + +Chef::Log.info "Keystone AdminURL: #{ks_admin_endpoint["uri"]}" +Chef::Log.info "Keystone InternalURL: #{ks_service_endpoint["uri"]}" +Chef::Log.info "Keystone PublicURL: #{ks_service_endpoint["uri"]}" + +keystone_register "Register Identity Endpoint" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + service_type "identity" + endpoint_region "RegionOne" + endpoint_adminurl node["keystone"]["adminURL"] + endpoint_internalurl node["keystone"]["internalURL"] + endpoint_publicurl node["keystone"]["publicURL"] + action :create_endpoint +end + + +node["keystone"]["users"].each do |username, user_info| + keystone_credentials "Create EC2 credentials for '#{username}' user" do + auth_host ks_admin_endpoint["host"] + auth_port ks_admin_endpoint["port"] + auth_protocol ks_admin_endpoint["schema"] + api_ver ks_admin_endpoint["path"] + auth_token node["keystone"]["admin_token"] + user_name username + tenant_name user_info["default_tenant"] + end +end + +# TODO(shep): this needs to be if blocked on env collectd toggle +# Include recipe(nova::network-monitoring) +include_recipe "keystone::server-monitoring" diff --git a/resources/credentials.rb b/resources/credentials.rb new file mode 100644 index 0000000..60e1254 --- /dev/null +++ b/resources/credentials.rb @@ -0,0 +1,17 @@ +actions :create_ec2 + +# In earlier versions of Chef the LWRP DSL doesn't support specifying +# a default action, so you need to drop into Ruby. +def initialize(*args) + super + @action = :create_ec2 +end + +attribute :auth_protocol, :kind_of => String, :equal_to => [ "http", "https" ] +attribute :auth_host, :kind_of => String +attribute :auth_port, :kind_of => String +attribute :api_ver, :kind_of => String, :default => "/v2.0" +attribute :auth_token, :kind_of => String + +attribute :tenant_name, :kind_of => String +attribute :user_name, :kind_of => String diff --git a/resources/register.rb b/resources/register.rb new file mode 100644 index 0000000..73275f1 --- /dev/null +++ b/resources/register.rb @@ -0,0 +1,44 @@ + +actions :create_service, :create_endpoint, :create_tenant, :create_user, :create_role, :grant_role + +# In earlier versions of Chef the LWRP DSL doesn't support specifying +# a default action, so you need to drop into Ruby. +def initialize(*args) + super + @action = :create +end + +attribute :auth_protocol, :kind_of => String, :equal_to => [ "http", "https" ] +attribute :auth_host, :kind_of => String +attribute :auth_port, :kind_of => String +attribute :api_ver, :kind_of => String, :default => "/v2.0" +attribute :auth_token, :kind_of => String + +# Used by both :create_service and :create_endpoint +attribute :service_type, :kind_of => String, :equal_to => [ "image", "identity", "compute", "storage", "ec2", "volume", "object-store" ] + +# :create_service specific attributes +attribute :service_name, :kind_of => String +attribute :service_description, :kind_of => String + +# :create_endpoint specific attributes +attribute :endpoint_region, :kind_of => String, :default => "RegionOne" +attribute :endpoint_adminurl, :kind_of => String +attribute :endpoint_internalurl, :kind_of => String +attribute :endpoint_publicurl, :kind_of => String + +# Used by both :create_tenant and :create_user +attribute :tenant_name, :kind_of => String + +# :create_tenant specific attributes +attribute :tenant_description, :kind_of => String +attribute :tenant_enabled, :kind_of => String, :equal_to => [ "true", "false" ], :default => "true" + +# :create_user specific attributes +attribute :user_name, :kind_of => String +attribute :user_pass, :kind_of => String +# attribute :user_email, :kind_of => String +attribute :user_enabled, :kind_of => String, :equal_to => [ "true", "false" ], :default => "true" + +# Used by :create_role and :grant_role specific attributes +attribute :role_name, :kind_of => String diff --git a/templates/default/keystone-logging.conf.erb b/templates/default/keystone-logging.conf.erb new file mode 100644 index 0000000..b2d5db2 --- /dev/null +++ b/templates/default/keystone-logging.conf.erb @@ -0,0 +1,49 @@ +[loggers] +keys=root,keystone,combined + +[formatters] +keys=normal,normal_with_name,debug + +[handlers] +keys=production,file,devel + +[logger_root] +level=NOTSET +handlers=file + +[logger_keystone] +level=DEBUG +handlers=file +qualname=keystone + +[logger_combined] +level=DEBUG +handlers=file +qualname=keystone-combined + +[handler_production] +class=handlers.SysLogHandler +level=ERROR +formatter=normal_with_name +args=(('localhost', handlers.SYSLOG_UDP_PORT), handlers.SysLogHandler.LOG_USER) + +[handler_file] +class=FileHandler +level=DEBUG +formatter=normal_with_name +args=('/var/log/keystone/keystone.log', 'w') + +[handler_devel] +class=StreamHandler +level=NOTSET +formatter=debug +args=(sys.stdout,) + +[formatter_normal] +format=%(asctime)s %(levelname)s %(message)s + +[formatter_normal_with_name] +format=(%(name)s): %(asctime)s %(levelname)s %(message)s + +[formatter_debug] +format=(%(name)s): %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s diff --git a/templates/default/keystone.conf.erb b/templates/default/keystone.conf.erb new file mode 100644 index 0000000..72896e3 --- /dev/null +++ b/templates/default/keystone.conf.erb @@ -0,0 +1,107 @@ +[DEFAULT] +public_port = <%= @service_port %> +admin_port = <%= @admin_port %> +admin_token = <%= @admin_token %> +bind_host = <%= @ip_address %> +compute_port = 8774 +verbose = True +debug = True +log_config = /etc/keystone/logging.conf + +# ================= Syslog Options ============================ +# Send logs to syslog (/dev/log) instead of to file specified +# by `log-file` +use_syslog = False + +# Facility to use. If unset defaults to LOG_USER. +# syslog_log_facility = LOG_LOCAL0 + +[sql] +connection = mysql://<%= @user %>:<%= @passwd %>@<%= @db_ipaddress %>/<%= @db_name %> +idle_timeout = 200 +min_pool_size = 5 +max_pool_size = 10 +pool_timeout = 200 + +[ldap] +#url = ldap://localhost +#tree_dn = dc=example,dc=com +#user_tree_dn = ou=Users,dc=example,dc=com +#role_tree_dn = ou=Roles,dc=example,dc=com +#tenant_tree_dn = ou=Groups,dc=example,dc=com +#user = dc=Manager,dc=example,dc=com +#password = freeipa4all +#suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.sql.Identity + +[catalog] +driver = keystone.catalog.backends.sql.Catalog + +[token] +driver = keystone.token.backends.sql.Token + +# Amount of time a token should remain valid (in seconds) +expiration = 86400 + +[policy] +driver = keystone.policy.backends.simple.SimpleMatch + +[ec2] +driver = keystone.contrib.ec2.backends.sql.Ec2 + +[filter:debug] +paste.filter_factory = keystone.common.wsgi:Debug.factory + +[filter:token_auth] +paste.filter_factory = keystone.middleware:TokenAuthMiddleware.factory + +[filter:admin_token_auth] +paste.filter_factory = keystone.middleware:AdminTokenAuthMiddleware.factory + +[filter:xml_body] +paste.filter_factory = keystone.middleware:XmlBodyMiddleware.factory + +[filter:json_body] +paste.filter_factory = keystone.middleware:JsonBodyMiddleware.factory + +[filter:crud_extension] +paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory + +[filter:ec2_extension] +paste.filter_factory = keystone.contrib.ec2:Ec2Extension.factory + +[app:public_service] +paste.app_factory = keystone.service:public_app_factory + +[app:admin_service] +paste.app_factory = keystone.service:admin_app_factory + +[pipeline:public_api] +pipeline = token_auth admin_token_auth xml_body json_body debug ec2_extension public_service + +[pipeline:admin_api] +pipeline = token_auth admin_token_auth xml_body json_body debug ec2_extension crud_extension admin_service + +[app:public_version_service] +paste.app_factory = keystone.service:public_version_app_factory + +[app:admin_version_service] +paste.app_factory = keystone.service:admin_version_app_factory + +[pipeline:public_version_api] +pipeline = xml_body public_version_service + +[pipeline:admin_version_api] +pipeline = xml_body admin_version_service + +[composite:main] +use = egg:Paste#urlmap +/v2.0 = public_api +/ = public_version_api + +[composite:admin] +use = egg:Paste#urlmap +/v2.0 = admin_api +/ = admin_version_api