Commit 9de489dd authored by Giorgos Korfiatis's avatar Giorgos Korfiatis
Browse files

Merge branch 'feature-projects-improved' into develop

parents 5089361f 5c8747a6
......@@ -16,6 +16,20 @@ Synnefo-wide
* Integrate Pithos tests in continuous integration.
Astakos
-------
* Changes in project schema:
* A Project entry is created when submitting an application for a new
project, rather than on approval. Its state is dependent on the state
of its `reference' application (current definition). Lock Project rather
than Chain (the latter is semantically obsolete).
* Improve recording of project, application, and membership actions.
* Implement API calls for projects.
Cyclades
--------
......
......@@ -69,6 +69,9 @@ API_SERVICE_QUOTAS = join_urls(ACCOUNTS_PREFIX, "service_quotas")
API_COMMISSIONS = join_urls(ACCOUNTS_PREFIX, "commissions")
API_COMMISSIONS_ACTION = join_urls(API_COMMISSIONS, "action")
API_FEEDBACK = join_urls(ACCOUNTS_PREFIX, "feedback")
API_PROJECTS = join_urls(ACCOUNTS_PREFIX, "projects")
API_APPLICATIONS = join_urls(API_PROJECTS, "apps")
API_MEMBERSHIPS = join_urls(API_PROJECTS, "memberships")
# --------------------------------------------------------------------
# Astakos Keystone API urls
......@@ -575,6 +578,235 @@ class AstakosClient():
self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# ----------------------------
# do a GET to ``API_PROJECTS``
def get_projects(self, token, name=None, state=None, owner=None):
"""Retrieve all accessible projects
Arguments:
token -- user's token (string)
name -- filter by name (optional)
state -- filter by state (optional)
owner -- filter by owner (optional)
In case of success, return a list of project descriptions.
"""
path = API_PROJECTS
filters = {}
if name is not None:
filters["name"] = name
if state is not None:
filters["state"] = state
if owner is not None:
filters["owner"] = owner
req_headers = {'content-type': 'application/json'}
req_body = (parse_request({"filter": filters}, self.logger)
if filters else None)
return self._call_astakos(token, path, req_headers, req_body)
# -----------------------------------------
# do a GET to ``API_PROJECTS``/<project_id>
def get_project(self, token, project_id):
"""Retrieve project description, if accessible
Arguments:
token -- user's token (string)
project_id -- project identifier
In case of success, return project description.
"""
path = join_urls(API_PROJECTS, str(project_id))
return self._call_astakos(token, path)
# -----------------------------
# do a POST to ``API_PROJECTS``
def create_project(self, token, specs):
"""Submit application to create a new project
Arguments:
token -- user's token (string)
specs -- dict describing a project
In case of success, return project and application identifiers.
"""
path = API_PROJECTS
req_headers = {'content-type': 'application/json'}
req_body = parse_request(specs, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# ------------------------------------------
# do a POST to ``API_PROJECTS``/<project_id>
def modify_project(self, token, project_id, specs):
"""Submit application to modify an existing project
Arguments:
token -- user's token (string)
project_id -- project identifier
specs -- dict describing a project
In case of success, return project and application identifiers.
"""
path = join_urls(API_PROJECTS, str(project_id))
req_headers = {'content-type': 'application/json'}
req_body = parse_request(specs, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# -------------------------------------------------
# do a POST to ``API_PROJECTS``/<project_id>/action
def project_action(self, token, project_id, action, reason=""):
"""Perform action on a project
Arguments:
token -- user's token (string)
project_id -- project identifier
action -- action to perform, one of "suspend", "unsuspend",
"terminate", "reinstate"
reason -- reason of performing the action
In case of success, return nothing.
"""
path = join_urls(API_PROJECTS, str(project_id))
path = join_urls(path, "action")
req_headers = {'content-type': 'application/json'}
req_body = parse_request({action: reason}, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# --------------------------------
# do a GET to ``API_APPLICATIONS``
def get_applications(self, token, project=None):
"""Retrieve all accessible applications
Arguments:
token -- user's token (string)
project -- filter by project (optional)
In case of success, return a list of application descriptions.
"""
path = API_APPLICATIONS
req_headers = {'content-type': 'application/json'}
body = {"project": project} if project is not None else None
req_body = parse_request(body, self.logger) if body else None
return self._call_astakos(token, path, req_headers, req_body)
# -----------------------------------------
# do a GET to ``API_APPLICATIONS``/<app_id>
def get_application(self, token, app_id):
"""Retrieve application description, if accessible
Arguments:
token -- user's token (string)
app_id -- application identifier
In case of success, return application description.
"""
path = join_urls(API_APPLICATIONS, str(app_id))
return self._call_astakos(token, path)
# -------------------------------------------------
# do a POST to ``API_APPLICATIONS``/<app_id>/action
def application_action(self, token, app_id, action, reason=""):
"""Perform action on an application
Arguments:
token -- user's token (string)
app_id -- application identifier
action -- action to perform, one of "approve", "deny",
"dismiss", "cancel"
reason -- reason of performing the action
In case of success, return nothing.
"""
path = join_urls(API_APPLICATIONS, str(app_id))
path = join_urls(path, "action")
req_headers = {'content-type': 'application/json'}
req_body = parse_request({action: reason}, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# -------------------------------
# do a GET to ``API_MEMBERSHIPS``
def get_memberships(self, token, project=None):
"""Retrieve all accessible memberships
Arguments:
token -- user's token (string)
project -- filter by project (optional)
In case of success, return a list of membership descriptions.
"""
path = API_MEMBERSHIPS
req_headers = {'content-type': 'application/json'}
body = {"project": project} if project is not None else None
req_body = parse_request(body, self.logger) if body else None
return self._call_astakos(token, path, req_headers, req_body)
# -----------------------------------------
# do a GET to ``API_MEMBERSHIPS``/<memb_id>
def get_membership(self, token, memb_id):
"""Retrieve membership description, if accessible
Arguments:
token -- user's token (string)
memb_id -- membership identifier
In case of success, return membership description.
"""
path = join_urls(API_MEMBERSHIPS, str(memb_id))
return self._call_astakos(token, path)
# -------------------------------------------------
# do a POST to ``API_MEMBERSHIPS``/<memb_id>/action
def membership_action(self, token, memb_id, action, reason=""):
"""Perform action on a membership
Arguments:
token -- user's token (string)
memb_id -- membership identifier
action -- action to perform, one of "leave", "cancel", "accept",
"reject", "remove"
reason -- reason of performing the action
In case of success, return nothing.
"""
path = join_urls(API_MEMBERSHIPS, str(memb_id))
path = join_urls(path, "action")
req_headers = {'content-type': 'application/json'}
req_body = parse_request({action: reason}, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# --------------------------------
# do a POST to ``API_MEMBERSHIPS``
def join_project(self, token, project_id):
"""Join a project
Arguments:
token -- user's token (string)
project_id -- project identifier
In case of success, return membership identifier.
"""
path = API_MEMBERSHIPS
req_headers = {'content-type': 'application/json'}
body = {"join": {"project": project_id}}
req_body = parse_request(body, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# --------------------------------
# do a POST to ``API_MEMBERSHIPS``
def enroll_member(self, token, project_id, email):
"""Enroll a user in a project
Arguments:
token -- user's token (string)
project_id -- project identifier
email -- user identified by email
In case of success, return membership identifier.
"""
path = API_MEMBERSHIPS
req_headers = {'content-type': 'application/json'}
body = {"enroll": {"project": project_id, "user": email}}
req_body = parse_request(body, self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
# --------------------------------------------------------------------
# Private functions
......
......@@ -192,6 +192,44 @@ retry=0, use_pool=False, pool_size=8, logger=None\ **)**
resolved.
Otherwise raise an AstakosClientException exception.
**get_projects(**\ token, name=None, state=None, owner=None\ **)**
Retrieve all accessible projects
**get_project(**\ token, project_id\ **)**
Retrieve project description, if accessible
**create_project(**\ token, specs\ **)**
Submit application to create a new project
**modify_project(**\ token, project_id, specs\ **)**
Submit application to modify an existing project
**project_action(**\ token, project_id, action, reason=""\ **)**
Perform action on a project
**get_applications(**\ token, project=None\ **)**
Retrieve all accessible applications
**get_application(**\ token, app_id\ **)**
Retrieve application description, if accessible
**application_action(**\ token, app_id, action, reason=""\ **)**
Perform action on an application
**get_memberships(**\ token, project=None\ **)**
Retrieve all accessible memberships
**get_membership(**\ token, memb_id\ **)**
Retrieve membership description, if accessible
**membership_action(**\ token, memb_id, action, reason=""\ **)**
Perform action on a membership
**join_project(**\ token, project_id\ **)**
Join a project
**enroll_member(**\ token, project_id, email\ **)**
Enroll a user in a project
Public Functions
----------------
......
......@@ -46,6 +46,13 @@ Resource and Quota Service API (Astakos)
Resource and Quota API <quota-api-guide.rst>
Project API
===========
.. toctree::
:maxdepth: 2
Project API <project-api-guide.rst>
Compute Service API (Cyclades)
==============================
......
Projects
--------
Astakos allows users to create *projects*. Through a project, one can ask for
additional resources on the virtual infrastructure for a certain amount of
time. All users admitted to the project gain access to these resources.
Retrieve List of Projects
.........................
**GET** /account/v1.0/projects
Returns all accessible projects. See below.
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
Request can specify a filter.
**Example Request**:
.. code-block:: javascript
{
"filter": {
"state": ["active", "suspended"],
"owner": [uuid]
}
}
**Response Codes**:
====== =====================
Status Description
====== =====================
200 Success
400 Bad Request
401 Unauthorized (Missing token)
500 Internal Server Error
====== =====================
**Example Successful Response**:
List of project details. See below.
Retrieve a Project
..................
**GET** /account/v1.0/projects/<proj_id>
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
A project is accessible when the request user is admin, project owner,
applicant or member, or the project is active.
**Response Codes**:
====== ============================
Status Description
====== ============================
200 Success
401 Unauthorized (Missing token)
403 Forbidden
404 Not Found
500 Internal Server Error
====== ============================
**Example Successful Response**:
.. code-block:: javascript
{
"id": proj_id,
"application": app_id,
"state": "pending" | "active" | "denied" | "dismissed" | "cancelled" | "suspended" | "terminated",
"creation_date": "2013-06-26T11:48:06.579100+00:00",
"name": "name",
"owner": uuid,
"homepage": homepage or null,
"description": description or null,
"start_date": date,
"end_date": date,
"join_policy": "auto" | "moderated" | "closed",
"leave_policy": "auto" | "moderated" | "closed",
"max_members": int or null
"resources": {"cyclades.vm": {"project_capacity": int or null,
"member_capacity": int
}
}
# only if request user is admin or project owner:
"comments": comments,
"pending_application": last pending app id or null,
"deactivation_date": date # if applicable
}
Create a Project
................
**POST** /account/v1.0/projects
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
**Example Request**:
.. code-block:: javascript
{
"name": name,
"owner": uuid, # if omitted, request user assumed
"homepage": homepage, # optional
"description": description, # optional
"comments": comments, # optional
"start_date": date, # optional
"end_date": date,
"join_policy": "auto" | "moderated" | "closed", # default: "moderated"
"leave_policy": "auto" | "moderated" | "closed", # default: "auto"
"resources": {"cyclades.vm": {"project_capacity": int or null,
"member_capacity": int
}
}
}
**Response Codes**:
====== ============================
Status Description
====== ============================
201 Created
400 Bad Request
401 Unauthorized (Missing token)
403 Forbidden
409 Conflict
500 Internal Server Error
====== ============================
**Example Successful Response**:
.. code-block:: javascript
{
"id": project_id,
"application": application_id
}
Modify a Project
................
**POST** /account/v1.0/projects/<proj_id>
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
**Example Request**:
As above.
**Response Codes**:
====== ============================
Status Description
====== ============================
201 Created
400 Bad Request
401 Unauthorized (Missing token)
403 Forbidden
404 Not Found
409 Conflict
500 Internal Server Error
====== ============================
**Example Successful Response**:
As above.
Take Action on a Project
........................
**POST** /account/v1.0/projects/<proj_id>/action
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
**Example Request**:
.. code-block:: javascript
{
<action>: "reason"
}
<action> can be: "suspend", "unsuspend", "terminate", "reinstate"
**Response Codes**:
====== ============================
Status Description
====== ============================
200 Success
400 Bad Request
401 Unauthorized (Missing token)
403 Forbidden
404 Not Found
409 Conflict
500 Internal Server Error
====== ============================
Retrieve List of Applications
.............................
**GET** /account/v1.0/projects/apps
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
Get all accessible applications. See below.
**Example optional request**
.. code-block:: javascript
{
"project": <project_id>
}
**Response Codes**:
====== ============================
Status Description
====== ============================
200 Success
400 Bad Request
401 Unauthorized (Missing token)
500 Internal Server Error
====== ============================
**Example Successful Response**:
List of application details. See below.
Retrieve an Application
.......................
**GET** /account/v1.0/projects/apps/<app_id>
==================== =========================
Request Header Name Value
==================== =========================
X-Auth-Token User authentication token
==================== =========================
An application is accessible when the request user is admin or the
application owner/applicant.
**Response Codes**:
====== ============================
Status Description
====== ============================
200 Success
401 Unauthorized (Missing token)
403 Forbidden
404 Not Found
500 Internal Server Error
====== ============================
**Example Successful Response**
.. code-block:: javascript
{
"id": app_id,
"project": project_id,
"state": "pending" | "approved" | "replaced" | "denied" | "dismissed" | "cancelled",
"name": "name",
"owner": uuid,
"applicant": uuid,
"homepage": homepage or null,
"description": description or null,
"start_date": date,
"end_date": date,