
A new method connection-tenant, returns the connections tenant as an object. This commit also fixes some problems with the keystone authentication function and adds more test coverage. Change-Id: I9b753a7c6e68be45cf1797e5966ac1ff2e2fca2f
451 lines
15 KiB
Common Lisp
451 lines
15 KiB
Common Lisp
(defpackage cl-keystone-client
|
|
(:use cl)
|
|
(:import-from #:cl-openstack-client
|
|
#:*http-stream*
|
|
#:assoc*
|
|
#:decode-resource
|
|
#:decode-resource-list
|
|
#:def-rest-method
|
|
#:error-code
|
|
#:error-message
|
|
#:handle-http-error
|
|
#:id
|
|
#:openstack-error
|
|
#:request-resource
|
|
#:resource
|
|
#:resource-authentication-headers
|
|
#:resource-connection
|
|
#:resource-error-class
|
|
#:resource-id
|
|
#:service-url)
|
|
(:import-from #:drakma
|
|
#:http-request)
|
|
(:import-from #:local-time
|
|
#:parse-timestring
|
|
#:timestamp>
|
|
#:now)
|
|
(:import-from #:cl-json
|
|
#:*json-input*
|
|
#:*json-identifier-name-to-lisp*
|
|
#:with-explicit-encoder
|
|
#:encode-json
|
|
#:encode-json-to-string)
|
|
(:import-from #:alexandria
|
|
#:alist-plist)
|
|
(:export connection-v2
|
|
authenticate
|
|
keystone-error
|
|
error-code
|
|
error-message
|
|
connection-username
|
|
connection-tenant-id
|
|
connection-tenant-name
|
|
connection-password
|
|
connection-url
|
|
connection-token-id
|
|
connection-token-expires
|
|
connection-token-issued-at
|
|
connection-token-valid-p
|
|
connection-tenant
|
|
connection-service-catalog
|
|
|
|
;; Resource Methods
|
|
resource-id
|
|
resource-name
|
|
resource-connection
|
|
resource-description
|
|
|
|
;; Resource Slots
|
|
id
|
|
name
|
|
enabled
|
|
description
|
|
|
|
;; Tenant Methods
|
|
tenant
|
|
tenant-id
|
|
tenant-name
|
|
tenant-enabled
|
|
tenant-description
|
|
list-tenants
|
|
|
|
;; User Methods
|
|
user-id
|
|
user-name
|
|
user-tenant
|
|
user-enabled
|
|
user-email
|
|
user-roles
|
|
add-user
|
|
get-user
|
|
delete-user
|
|
list-users
|
|
|
|
;; Role Methods
|
|
role-id
|
|
role-name
|
|
role-enabled
|
|
list-roles))
|
|
|
|
(in-package :cl-keystone-client)
|
|
|
|
(define-condition keystone-error (openstack-error) ())
|
|
|
|
(defclass connection ()
|
|
((username :initarg :username
|
|
:reader connection-username
|
|
:initform (error ":USERNAME is required when creating a connection."))
|
|
(tenant-id :initarg :tenant-id
|
|
:reader connection-tenant-id)
|
|
(tenant-name :initarg :tenant-name
|
|
:reader connection-tenant-name)
|
|
(password :initarg :password
|
|
:initform (error ":PASSWORD is required when creating a connection.")
|
|
:reader connection-password)
|
|
(endpoint :initarg :endpoint
|
|
:initform :public-url
|
|
:reader connection-endpoint)
|
|
(token)
|
|
(user)
|
|
(metadata)
|
|
(service-catalog :reader connection-service-catalog)
|
|
(url :initarg :url
|
|
:reader connection-url
|
|
:initform (error ":URL is required when creating a connection."))))
|
|
|
|
;; Add API compatability with the resource object
|
|
(defmethod resource-connection ((connection connection))
|
|
connection)
|
|
|
|
(defmethod resource-error-class ((resource connection))
|
|
'keystone-error)
|
|
|
|
(defmethod encode-json ((connection connection)
|
|
&optional (stream json:*json-output*))
|
|
"Write the JSON representation (Object) of the keystone CONNECTION
|
|
to STREAM (or to *JSON-OUTPUT*)."
|
|
(with-slots (username password) connection
|
|
(with-explicit-encoder
|
|
(encode-json
|
|
`(:object
|
|
:auth
|
|
(:object
|
|
:password-credentials
|
|
(:object
|
|
:username ,username
|
|
:password ,password)
|
|
,@(cond
|
|
((slot-boundp connection 'tenant-id)
|
|
(list :tenant-id (connection-tenant-id connection)))
|
|
((slot-boundp connection 'tenant-name)
|
|
(list :tenant-name (connection-tenant-name connection))))))
|
|
stream))))
|
|
|
|
(defclass connection-v2 (connection)
|
|
((version :initform 2 :reader connection-version)))
|
|
|
|
(defmethod headers-for ((connection connection-v2) &optional action)
|
|
(declare (ignore action))
|
|
nil)
|
|
|
|
(defun openstack-camel-case-to-lisp (camel-string)
|
|
"Convert camel case JSON keys to lisp symbol names. This function
|
|
handles keys with names like publicURL better and will convert keys
|
|
with underscores to hyphens."
|
|
(declare (string camel-string))
|
|
(let ((*print-pretty* nil))
|
|
(with-output-to-string (result)
|
|
(loop :for c :across camel-string
|
|
:with last-was-lowercase
|
|
:when (and last-was-lowercase
|
|
(upper-case-p c))
|
|
:do (princ "-" result)
|
|
:if (lower-case-p c)
|
|
:do (setf last-was-lowercase t)
|
|
:else
|
|
:do (setf last-was-lowercase nil)
|
|
:if (member c (list #\_))
|
|
:do (princ "-" result)
|
|
:else
|
|
:do (princ (char-upcase c) result)))))
|
|
|
|
(defun decode-json (&optional (stream *json-input*))
|
|
(let ((*json-identifier-name-to-lisp* #'openstack-camel-case-to-lisp))
|
|
(cl-json:decode-json stream)))
|
|
|
|
(defgeneric authenticate (connection)
|
|
(:documentation "Authenticate and retrieve a token."))
|
|
|
|
(defmethod authenticate ((connection connection-v2))
|
|
(with-slots (url token user service-catalog metadata) connection
|
|
(multiple-value-bind (body status-code headers uri stream must-close reason-phrase)
|
|
(http-request (format nil "~a/v2.0/tokens" url)
|
|
:method :POST
|
|
:want-stream t
|
|
:stream *http-stream*
|
|
:content-type "application/json"
|
|
:content
|
|
(encode-json-to-string connection))
|
|
(declare (ignore must-close reason-phrase body))
|
|
(handle-http-error connection uri status-code headers stream)
|
|
(let ((access (assoc* :access (decode-json stream))))
|
|
(setf user (assoc* :user access))
|
|
(setf service-catalog (assoc* :service-catalog access))
|
|
(setf metadata (assoc* :metadata access))
|
|
(setf token (assoc* :token access)))))
|
|
connection)
|
|
|
|
(defgeneric connection-token-id (connection)
|
|
(:documentation "Retrieve token id for CONNECTION."))
|
|
|
|
(defmethod connection-token-id ((connection connection-v2))
|
|
(assoc* :id (slot-value connection 'token)))
|
|
|
|
(defgeneric connection-token-issued-at (connection)
|
|
(:documentation "Return the time the CONNECTION's token was issued
|
|
at."))
|
|
|
|
(defmethod connection-token-issued-at ((connection connection-v2))
|
|
(parse-timestring (assoc* :issued-at (slot-value connection 'token))))
|
|
|
|
(defgeneric connection-token-expires (connection)
|
|
(:documentation "Return the time when the CONNECTION's token will
|
|
expire."))
|
|
|
|
(defmethod connection-token-expires ((connection connection-v2))
|
|
(parse-timestring (assoc* :expires (slot-value connection 'token))))
|
|
|
|
(defgeneric connection-token-valid-p (connection)
|
|
(:documentation "Return T if the CONNECTION's token is still
|
|
valid."))
|
|
|
|
(defmethod connection-token-valid-p ((connection connection-v2))
|
|
(timestamp>
|
|
(connection-token-expires connection)
|
|
(now)))
|
|
|
|
(defmethod connection-tenant ((connection connection-v2))
|
|
"Return the current connections TENANT."
|
|
(apply #'make-instance
|
|
'tenant-v2
|
|
:connection connection
|
|
(alist-plist (assoc* :tenant (slot-value connection 'token)))))
|
|
|
|
(defmethod resource-authentication-headers ((resource connection-v2))
|
|
`(("x-auth-token" . ,(connection-token-id resource))))
|
|
|
|
;; Service catalog queries
|
|
|
|
(defun filter-endpoints (endpoints &key (type :public-url) region)
|
|
(loop :for endpoint :in endpoints
|
|
:when (or (not region)
|
|
(equal (assoc* :region endpoint) region))
|
|
:collect (assoc* type endpoint)))
|
|
|
|
(defmethod service-catalog-query ((connection connection-v2) service-type &key (type :public-url))
|
|
(loop :for service :in (connection-service-catalog connection)
|
|
:when (equal (assoc* :type service) service-type)
|
|
:append (filter-endpoints (assoc* :endpoints service)
|
|
:type type)))
|
|
|
|
(defmethod service-url ((connection connection-v2) &optional (service "identity"))
|
|
(car (service-catalog-query connection service
|
|
:type (connection-endpoint connection))))
|
|
|
|
|
|
(defclass resource-v2 (resource)
|
|
())
|
|
|
|
(defmethod resource-error-class ((resource resource-v2))
|
|
'keystone-error)
|
|
|
|
(defmethod resource-authentication-headers ((resource resource-v2))
|
|
(resource-authentication-headers (resource-connection resource)))
|
|
|
|
(defmethod service-url ((resource resource-v2) &optional (service "identity"))
|
|
(service-url (resource-connection resource) service))
|
|
|
|
|
|
(defclass named-resource-v2 (resource-v2)
|
|
((name :initarg :name :reader resource-name)))
|
|
|
|
(defmethod print-object ((resource named-resource-v2) stream)
|
|
(if (slot-boundp resource 'name)
|
|
(print-unreadable-object
|
|
(resource stream :type t :identity t)
|
|
(format stream "~a"
|
|
(cond
|
|
((and (slot-exists-p resource 'name)
|
|
(slot-boundp resource 'name))
|
|
(slot-value resource 'name))
|
|
((and (slot-exists-p resource 'id)
|
|
(slot-boundp resource 'id))
|
|
(slot-value resource 'id))
|
|
(t "UNKNOWN"))))
|
|
(print-unreadable-object (resource stream :type t :identity t))))
|
|
|
|
|
|
;; Tenants
|
|
|
|
(defclass tenant (named-resource-v2)
|
|
((id :initarg :id :reader tenant-id)
|
|
(name :initarg :name :reader tenant-name)
|
|
(enabled :initarg :enabled :reader tenant-enabled)
|
|
(description :initarg :description :reader tenant-description)))
|
|
|
|
(defclass tenant-v2 (tenant)
|
|
())
|
|
|
|
(defmethod encode-json ((tenant tenant-v2)
|
|
&optional (stream json:*json-output*))
|
|
"Write the JSON representation (Object) of the keystone CONNECTION
|
|
to STREAM (or to *JSON-OUTPUT*)."
|
|
(with-slots (id name enabled description) tenant
|
|
(with-explicit-encoder
|
|
(encode-json
|
|
`(:object
|
|
:tenant
|
|
(:object
|
|
:id ,id
|
|
:name ,name
|
|
:description ,description
|
|
:enabled ,enabled))
|
|
stream))))
|
|
|
|
(defmethod decode-resource ((type (eql 'tenant-v2)) (parent connection-v2) stream)
|
|
(loop :for tenant :in (assoc* :tenants (decode-json stream))
|
|
:collect (apply #'make-instance
|
|
type
|
|
:connection parent
|
|
(alist-plist tenant))))
|
|
|
|
(defgeneric list-tenants (resource))
|
|
|
|
(def-rest-method list-tenants ((connection connection-v2))
|
|
((:documentation "List all the tenants.")
|
|
(:uri "/tenants"))
|
|
(let ((json (request-resource connection :method :get)))
|
|
(decode-resource-list (assoc* :tenants json)
|
|
connection 'tenant-v2)))
|
|
|
|
;; Users
|
|
|
|
(defclass user (named-resource-v2)
|
|
((name :initarg :name :reader user-name)
|
|
(tenant-id :initarg :tenant-id :reader user-tenant)
|
|
(enabled :initarg :enabled :reader user-enabled)
|
|
(email :initarg :email :reader user-email)))
|
|
|
|
(defmethod user-id ((user user))
|
|
(resource-id user))
|
|
|
|
(defclass user-v2 (user)
|
|
())
|
|
|
|
|
|
;;; Make the connection behave like the current user.
|
|
|
|
(defmethod user-id ((connection connection-v2))
|
|
(assoc* :id (slot-value connection 'user)))
|
|
|
|
(defmethod user-name ((connection connection-v2))
|
|
(assoc* :name (slot-value connection 'user)))
|
|
|
|
(defgeneric list-users (resource))
|
|
|
|
(def-rest-method list-users ((tenant tenant-v2))
|
|
((:documentation "List all the users for tenant.")
|
|
(:uri "/tenants/{tenant}/users"))
|
|
(let ((json (request-resource tenant
|
|
:method :get)))
|
|
(decode-resource-list (assoc* :users json)
|
|
tenant
|
|
'user-v2)))
|
|
|
|
(def-rest-method list-users ((connection connection-v2))
|
|
((:documentation "List all users in keystone.")
|
|
(:uri "/users"))
|
|
(let ((json (request-resource connection :method :get)))
|
|
(decode-resource-list (assoc* :users json)
|
|
connection
|
|
'user-v2)))
|
|
|
|
(def-rest-method get-user (connection user)
|
|
((:documentation "Gets information for a specified user.")
|
|
(:uri "/users/{user}"))
|
|
(let ((json (request-resource connection :method :get)))
|
|
(decode-resource (assoc* :user json)
|
|
connection
|
|
'user-v2)))
|
|
|
|
(defgeneric add-user (connection &key name email enabled password))
|
|
|
|
(def-rest-method add-user ((connection connection-v2) &key name email (enabled t) password)
|
|
((:documentation "Add a user.")
|
|
(:uri "/users"))
|
|
(let ((json (request-resource
|
|
connection
|
|
:method :post
|
|
:content (with-output-to-string (stream)
|
|
(with-explicit-encoder
|
|
(encode-json
|
|
`(:object
|
|
:user
|
|
(:object
|
|
:name ,name
|
|
:email ,email
|
|
:enabled ,enabled
|
|
:password ,password))
|
|
stream))))))
|
|
(decode-resource (assoc* :user json)
|
|
connection
|
|
'user-v2)))
|
|
|
|
|
|
(defgeneric delete-user (resource user-or-user-id))
|
|
|
|
(def-rest-method delete-user ((connection connection-v2) user-or-user-id)
|
|
((:documentation "Delete a user.")
|
|
(:uri "/users/{user-or-user-id}"))
|
|
(request-resource connection :method :delete))
|
|
|
|
;; Roles
|
|
|
|
(defclass role (named-resource-v2)
|
|
((id :initarg :id :reader role-id)
|
|
(name :initarg :name :reader role-name)
|
|
(enabled :initarg :enabled :reader role-enabled)))
|
|
|
|
(defclass role-v2 (role)
|
|
())
|
|
|
|
(defgeneric add-tenant-users-role (tenant user role))
|
|
|
|
(def-rest-method add-tenant-users-role (tenant user role)
|
|
((:documentation "Adds a specified role to a user for a tenant.")
|
|
(:uri "/tenants/{tenant}/users/{user}/roles/OS-KSADM/{role}"))
|
|
(request-resource tenant :method :put))
|
|
|
|
(defgeneric delete-tenants-user-role (tenant user role))
|
|
|
|
(def-rest-method delete-tenants-user-role (tenant user role)
|
|
((:documentation "Deletes a specified role from a user on a tenant.")
|
|
(:uri "/tenants/{tenant}/users/{user}/roles/OS-KSADM/{role}"))
|
|
(request-resource tenant :method :delete))
|
|
|
|
(defgeneric list-roles (resource))
|
|
|
|
(def-rest-method list-roles ((connection connection-v2))
|
|
((:documentation "List roles.")
|
|
(:uri "/OS-KSADM/roles/"))
|
|
(let ((json (request-resource connection :method :get)))
|
|
(decode-resource-list (assoc* :roles json) connection 'role-v2)))
|
|
|
|
|
|
(def-rest-method list-roles ((user user-v2))
|
|
((:documentation "Lists global roles for a specified user. Excludes
|
|
tenant roles.")
|
|
(:uri "/users/{user}/roles"))
|
|
(let ((json (request-resource user :method :get)))
|
|
(decode-resource (assoc* :roles json) user 'role-v2)))
|