Initial commit
Project LaOS aka Lambdas-on-OpenStack
This commit is contained in:
commit
4471a4081a
29
.coveragerc
Normal file
29
.coveragerc
Normal file
@ -0,0 +1,29 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
source=laos
|
||||
omit=laos/tests*,
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
ignore_errors = False
|
||||
|
||||
[html]
|
||||
directory=cover
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.pyc
|
||||
.testrepository
|
||||
.tox/*
|
||||
dist/*
|
||||
build/*
|
||||
html/*
|
||||
*.egg*
|
||||
cover/*
|
||||
.coverage
|
||||
rdserver.txt
|
||||
python-troveclient.iml
|
||||
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
||||
.coverage.*
|
6
.testr.conf
Normal file
6
.testr.conf
Normal file
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
test_command=PYTHONASYNCIODEBUG=${PYTHONASYNCIODEBUG:-1} \
|
||||
${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
|
||||
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
176
LICENSE
Normal file
176
LICENSE
Normal file
@ -0,0 +1,176 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
155
README.md
Normal file
155
README.md
Normal file
@ -0,0 +1,155 @@
|
||||
Project LaOS aka Lambdas-on-OpenStack
|
||||
=====================================
|
||||
|
||||
Mission
|
||||
-------
|
||||
|
||||
Provide capabilities to run software in "serverless" way.
|
||||
|
||||
Serverless
|
||||
----------
|
||||
|
||||
Serverless is a new paradigm in computing that enables simplicity,
|
||||
efficiency and scalability for both developers and operators.
|
||||
It's important to distinguish the two, because the benefits differ:
|
||||
|
||||
Benefits for developers
|
||||
-----------------------
|
||||
|
||||
The main benefits that most people refer to are on the developer side and they include:
|
||||
|
||||
* No servers to manage (serverless) -- you just upload your code and the platform deals with the infrastructure
|
||||
* Super simple coding -- no more monoliths! Just simple little bits of code
|
||||
* Pay by the milliseconds your code is executing -- unlike a typical application that runs 24/7, and you're paying
|
||||
24/7, functions only run when needed
|
||||
|
||||
Benefits for operators
|
||||
----------------------
|
||||
|
||||
If you will be operating IronFunctions (the person who has to manage the servers behind the serverless),
|
||||
then the benefits are different, but related.
|
||||
|
||||
* Extremely efficient use of resources
|
||||
* Unlike an app/API/microservice that consumes resources 24/7 whether they
|
||||
are in use or not, functions are time sliced across your infrastructure and only consume resources while they are
|
||||
actually doing something
|
||||
* Easy to manage and scale
|
||||
* Single system for code written in any language or any technology
|
||||
* Single system to monitor
|
||||
* Scaling is the same for all functions, you don't scale each app independently
|
||||
* Scaling is simply adding more IronFunctions nodes
|
||||
|
||||
System requirements
|
||||
-------------------
|
||||
|
||||
* Operating system: Linux/MacOS
|
||||
* Python version: 3.5 or greater
|
||||
* Database: MySQL 5.7 or greater
|
||||
|
||||
Quick-start guide
|
||||
-----------------
|
||||
|
||||
Install DevStack with [IronFunctions enabled](https://github.com/iron-io/functions-devstack-plugin/blob/master/README.rst).
|
||||
Pull down [Project LaOS sources](https://github.com/denismakogon/project-laos).
|
||||
|
||||
Create Python3.5 virtualenv:
|
||||
|
||||
$ virtualenv -p python3.5 .venv
|
||||
$ source .venv/bin/activate
|
||||
|
||||
Install dependencies:
|
||||
|
||||
$ pip install -r requirements.txt -r test-requirements.txt
|
||||
|
||||
Install `functions_python` lib:
|
||||
|
||||
$ pip install -e git+ssh://git@github.com/iron-io/functions_python.git#egg=functions-python
|
||||
|
||||
Install LaOS itself:
|
||||
|
||||
$ pip install -e .
|
||||
|
||||
|
||||
Migrations
|
||||
----------
|
||||
|
||||
Once all dependencies are installed it is necessary to run database migrations.
|
||||
Before that please adjust [alembic.ini](alembic.ini) row #32
|
||||
|
||||
sqlalchemy.url = mysql+pymysql://root:root@192.168.0.112/functions
|
||||
|
||||
In this section please specify connection URI to your own database.
|
||||
Once it is done just hit alembic to apply migrations:
|
||||
|
||||
$ alembic upgrade head
|
||||
|
||||
Starting a server
|
||||
-----------------
|
||||
|
||||
Once it is finished you will have a console script `laos-api`:
|
||||
|
||||
$ laos-api --help
|
||||
|
||||
Usage: laos-api [OPTIONS]
|
||||
|
||||
Starts an Project Laos API service
|
||||
|
||||
Options:
|
||||
--host TEXT API service bind host.
|
||||
--port INTEGER API service bind port.
|
||||
--db-uri TEXT LaOS persistence storage URI.
|
||||
--keystone-endpoint TEXT OpenStack Identity service endpoint.
|
||||
--functions-host TEXT Functions API host
|
||||
--functions-port INTEGER Functions API port
|
||||
--functions-api-version TEXT Functions API version
|
||||
--functions-api-protocol TEXT Functions API protocol
|
||||
--log-level TEXT Logging file
|
||||
--log-file TEXT Log file path
|
||||
--help Show this message and exit.
|
||||
|
||||
Minimum required options to start LaOS API service:
|
||||
|
||||
--db-uri mysql://root:root@192.168.0.112/functions
|
||||
--keystone-endpoint http://192.168.0.112:5000/v3
|
||||
--functions-host 192.168.0.112
|
||||
--functions-port 10501
|
||||
--log-level INFO
|
||||
|
||||
Examining API
|
||||
-------------
|
||||
|
||||
In [examples](examples/) folder you can find a script that examines available API endpoints, but this script relays on:
|
||||
|
||||
* `LAOS_API_URL` - Project LaOS API endpoint
|
||||
* `OS_AUTH_URL` - OpenStack Auth URL
|
||||
* `OS_PROJECT_ID` - it can be found in OpenStack Dashboard or in CLI
|
||||
|
||||
Along with that, you need to adjust [token_request.json](examples/token_request.json) in order to retrieve X-Auth-Token for further authentication against OpenStack and LaOS API service.
|
||||
|
||||
Then just run script:
|
||||
|
||||
OS_AUTH_URL=http://192.168.0.112:5000/v3 OS_PROJECT_ID=8fb76785313a4500ac5367eb44a31677 ./hello-lambda.sh
|
||||
|
||||
Please note, that given values are project-specific, so they can't be reused.
|
||||
|
||||
|
||||
TODOs
|
||||
-----
|
||||
|
||||
* Create aiohttp swagger API using [aiohttp-swagger](https://github.com/cr0hn/aiohttp-swagger)
|
||||
* Support app deletion in IronFunctions
|
||||
* Support tasks listing/showing
|
||||
* Tests: integration, functional, units
|
||||
* better logging coverage
|
||||
* Support logging instance passing in [function-python](https://github.com/iron-io/functions_python)
|
||||
* python-laosclient (ReST API client and CLI tool)
|
||||
* App writing examples
|
||||
|
||||
|
||||
Contacts
|
||||
--------
|
||||
|
||||
Feel free to reach us out at:
|
||||
|
||||
* [Slack channel](https://open-iron.herokuapp.com/)
|
||||
* [Email](https://github.com/denismakogon)
|
68
alembic.ini
Normal file
68
alembic.ini
Normal file
@ -0,0 +1,68 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = mysql+pymysql://root:root@192.168.0.112/functions
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
51
examples/hello-lambda.sh
Executable file
51
examples/hello-lambda.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set +x
|
||||
|
||||
export LAOS_API_URL=${LAOS_API_URL:-http://localhost:10001}
|
||||
|
||||
export OS_AUTH_URL=${OS_AUTH_URL:-http://localhost:5000/v3}
|
||||
export OS_PROJECT_ID=${OS_PROJECT_ID:-"dummy_project_id"}
|
||||
export OS_TOKEN=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | awk '/X-Subject-Token/ {print $2}'`
|
||||
|
||||
echo -e "Listing apps\n"
|
||||
curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Creating app\n"
|
||||
curl -X POST -d '{"app":{"name": "testapp"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Listing apps\n"
|
||||
curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Showing app info\n"
|
||||
export raw_app_info=`curl localhost:10001/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool | grep name | awk '{print $2}'`
|
||||
export app_name=${raw_app_info:1:30}
|
||||
unset $raw_app_info
|
||||
curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name} -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Listing app routes\n"
|
||||
curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Creating app sync route\n"
|
||||
curl -X POST -d '{"route":{"type": "sync", "path": "/hello-sync", "image": "iron/hello"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/route -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json"
|
||||
|
||||
echo -e "Listing app routes\n"
|
||||
curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Show app route\n"
|
||||
curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes/hello-sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Running app sync route\n"
|
||||
curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/r/${PROJECT_ID}/${APP_NAME}/hello-sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Deleting app route\n"
|
||||
curl -X DELETE ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes/hello_sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Creating app async route\n"
|
||||
curl -X POST -d '{"route":{"type": "async", "path": "/hello-async", "image": "iron/hello"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/route -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Running app async route\n"
|
||||
curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/r/${PROJECT_ID}/${APP_NAME}/hello-async -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
||||
|
||||
echo -e "Deleting app\n"
|
||||
curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name} -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool
|
20
examples/token_request.json
Normal file
20
examples/token_request.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"auth": {
|
||||
"identity": {
|
||||
"methods": ["password"],
|
||||
"password": {
|
||||
"user": {
|
||||
"name": "admin",
|
||||
"domain": { "id": "default" },
|
||||
"password": "root"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"project": {
|
||||
"name": "admin",
|
||||
"domain": { "id": "default" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
laos/__init__.py
Normal file
0
laos/__init__.py
Normal file
0
laos/api/__init__.py
Normal file
0
laos/api/__init__.py
Normal file
0
laos/api/controllers/__init__.py
Normal file
0
laos/api/controllers/__init__.py
Normal file
146
laos/api/controllers/apps.py
Normal file
146
laos/api/controllers/apps.py
Normal file
@ -0,0 +1,146 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from laos.api.views import app as app_view
|
||||
|
||||
from laos.common.base import controllers
|
||||
from laos.common import config
|
||||
|
||||
from laos.models import app as app_model
|
||||
|
||||
|
||||
class AppV1Controller(controllers.ServiceControllerBase):
|
||||
|
||||
controller_name = "apps"
|
||||
version = "v1"
|
||||
|
||||
@controllers.api_action(method='GET', route='{project_id}/apps')
|
||||
async def list(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
log.info("Listing apps for project: {}".format(project_id))
|
||||
stored_apps = await app_model.Apps.find_by(project_id=project_id)
|
||||
final = []
|
||||
for app in stored_apps:
|
||||
fn_app = await fnclient.apps.show(app.name, loop=c.event_loop)
|
||||
final.append(app_view.AppView(app, fn_app).view())
|
||||
return web.json_response(
|
||||
data={
|
||||
self.controller_name: final,
|
||||
'message': "Successfully listed applications"
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
||||
@controllers.api_action(method='POST', route='{project_id}/apps')
|
||||
async def create(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
data = await request.json()
|
||||
log.info("Creating an app for project: {} with data {}"
|
||||
.format(project_id, str(data)))
|
||||
app_name = "{}-{}".format(
|
||||
data["app"]["name"],
|
||||
project_id)[:30]
|
||||
|
||||
if await app_model.Apps.exists(app_name, project_id):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "App {0} already exists".format(app_name)
|
||||
}
|
||||
}, status=409)
|
||||
|
||||
fn_app = await fnclient.apps.create(app_name, loop=c.event_loop)
|
||||
stored_app = await app_model.Apps(
|
||||
name=app_name, project_id=project_id,
|
||||
description=data["app"].get(
|
||||
"description",
|
||||
"App for project {}".format(
|
||||
project_id))).save()
|
||||
|
||||
return web.json_response(
|
||||
data={
|
||||
"app": app_view.AppView(stored_app, fn_app).view(),
|
||||
"message": "App successfully created",
|
||||
}, status=200
|
||||
)
|
||||
|
||||
@controllers.api_action(method='GET', route='{project_id}/apps/{app}')
|
||||
async def get(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
log.info("Requesting an app for project: {} with name {}"
|
||||
.format(project_id, app))
|
||||
|
||||
if not (await app_model.Apps.exists(app, project_id)):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "App {0} not found".format(app),
|
||||
}
|
||||
}, status=404)
|
||||
|
||||
stored_app = (await app_model.Apps.find_by(
|
||||
project_id=project_id, name=app)).pop()
|
||||
fn_app = await fnclient.apps.show(app, loop=c.event_loop)
|
||||
return web.json_response(
|
||||
data={
|
||||
"app": app_view.AppView(stored_app, fn_app).view(),
|
||||
"message": "Successfully loaded app",
|
||||
},
|
||||
status=200
|
||||
)
|
||||
# TODO(denismakogon): disabled until iron-io/functions/pull/259
|
||||
#
|
||||
# @controllers.api_action(method='PUT', route='{project_id}/apps/{app}')
|
||||
# async def update(self, request, **kwargs):
|
||||
# log = config.Config.config_instance().logger
|
||||
# project_id = request.match_info.get('project_id')
|
||||
# app = request.match_info.get('app')
|
||||
# data = await request.json()
|
||||
# log.info("Updating an app {} for project: {} with data {}"
|
||||
# .format(app, project_id, str(data)))
|
||||
# return web.json_response(
|
||||
# data={
|
||||
# "app": {}
|
||||
# },
|
||||
# status=200
|
||||
# )
|
||||
|
||||
@controllers.api_action(method='DELETE', route='{project_id}/apps/{app}')
|
||||
async def delete(self, request, **kwargs):
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
|
||||
if not (await app_model.Apps.exists(app, project_id)):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "App {0} not found".format(app),
|
||||
}
|
||||
}, status=404)
|
||||
|
||||
await app_model.Apps.delete(
|
||||
project_id=project_id, name=app)
|
||||
# TODO(denismakogon): enable DELETE to IronFunctions when once
|
||||
# https://github.com/iron-io/functions/issues/274 implemented
|
||||
# fn_app = await fnclient.apps.delete(app, loop=c.event_loop)
|
||||
return web.json_response(
|
||||
data={
|
||||
"message": "App successfully deleted",
|
||||
}, status=200)
|
160
laos/api/controllers/routes.py
Normal file
160
laos/api/controllers/routes.py
Normal file
@ -0,0 +1,160 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from laos.api.views import app as app_view
|
||||
|
||||
from laos.common.base import controllers
|
||||
from laos.common import config
|
||||
|
||||
from laos.models import app as app_model
|
||||
|
||||
|
||||
class AppRouteV1Controller(controllers.ServiceControllerBase):
|
||||
|
||||
controller_name = "routes"
|
||||
version = "v1"
|
||||
|
||||
@controllers.api_action(
|
||||
method='GET', route='{project_id}/apps/{app}/routes')
|
||||
async def list(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
if not (await app_model.Apps.exists(app, project_id)):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "App {0} not found".format(app),
|
||||
}
|
||||
}, status=404)
|
||||
|
||||
fn_app_routes = (await (await fnclient.apps.show(
|
||||
app, loop=c.event_loop)).routes.list(loop=c.event_loop))
|
||||
|
||||
log.info("Listing app {} routes for project: {}."
|
||||
.format(app, project_id))
|
||||
return web.json_response(data={
|
||||
"routes": app_view.AppRouteView(fn_app_routes).view(),
|
||||
"message": "Successfully loaded app routes",
|
||||
}, status=200)
|
||||
|
||||
@controllers.api_action(
|
||||
method='POST', route='{project_id}/apps/{app}/routes')
|
||||
async def create(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
if not (await app_model.Apps.exists(app, project_id)):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "App {0} not found".format(app),
|
||||
}
|
||||
}, status=404)
|
||||
|
||||
data = (await request.json())['route']
|
||||
path = data['path']
|
||||
|
||||
try:
|
||||
fn_app = await fnclient.apps.show(app, loop=c.event_loop)
|
||||
except Exception as ex:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": getattr(ex, "reason", str(ex)),
|
||||
}
|
||||
}, status=getattr(ex, "status", 500))
|
||||
|
||||
try:
|
||||
await fn_app.routes.show(path, loop=c.event_loop)
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": (
|
||||
"App {} route {} already exist"
|
||||
.format(app, path)
|
||||
)
|
||||
}
|
||||
}, status=409)
|
||||
except Exception as ex:
|
||||
if getattr(ex, "status", 500) != 404:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": getattr(ex, "reason", str(ex)),
|
||||
}
|
||||
}, status=getattr(ex, "status", 500))
|
||||
|
||||
new_fn_route = (await (await fnclient.apps.show(
|
||||
app, loop=c.event_loop)).routes.create(
|
||||
**data, loop=c.event_loop))
|
||||
|
||||
log.info("Creating new route in app {} "
|
||||
"for project: {} with data {}"
|
||||
.format(app, project_id, str(data)))
|
||||
return web.json_response(data={
|
||||
"route": app_view.AppRouteView([new_fn_route]).view(),
|
||||
"message": "App route successfully created"
|
||||
}, status=200)
|
||||
|
||||
@controllers.api_action(
|
||||
method='GET', route='{project_id}/apps/{app}/routes/{route}')
|
||||
async def get(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
path = request.match_info.get('route')
|
||||
|
||||
try:
|
||||
fn_app = await fnclient.apps.show(app, loop=c.event_loop)
|
||||
route = await fn_app.routes.show(
|
||||
"/{}".format(path), loop=c.event_loop)
|
||||
except Exception as ex:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": getattr(ex, "reason", str(ex)),
|
||||
}
|
||||
}, status=getattr(ex, "status", 500))
|
||||
|
||||
log.info("Requesting route {} in app {} for project: {}"
|
||||
.format(path, app, project_id))
|
||||
return web.json_response(data={
|
||||
"route": app_view.AppRouteView([route]).view().pop(),
|
||||
"message": "App route successfully loaded"
|
||||
}, status=200)
|
||||
|
||||
@controllers.api_action(
|
||||
method='DELETE', route='{project_id}/apps/{app}/routes/{route}')
|
||||
async def delete(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
log, fnclient = c.logger, c.functions_client
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
path = request.match_info.get('route')
|
||||
log.info("Deleting route {} in app {} for project: {}"
|
||||
.format(path, app, project_id))
|
||||
try:
|
||||
fn_app = await fnclient.apps.show(app, loop=c.event_loop)
|
||||
await fn_app.routes.show("/{}".format(path), loop=c.event_loop)
|
||||
await fn_app.routes.delete("/{}".format(path), loop=c.event_loop)
|
||||
except Exception as ex:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": getattr(ex, "reason", str(ex)),
|
||||
}
|
||||
}, status=getattr(ex, "status", 500))
|
||||
|
||||
return web.json_response(data={
|
||||
"message": "Route successfully deleted",
|
||||
}, status=200)
|
70
laos/api/controllers/runnable.py
Normal file
70
laos/api/controllers/runnable.py
Normal file
@ -0,0 +1,70 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from laos.common.base import controllers
|
||||
from laos.common import config
|
||||
|
||||
|
||||
class RunnableV1Controller(controllers.ServiceControllerBase):
|
||||
|
||||
controller_name = "runnable"
|
||||
# IronFunction uses `r` as runnable instead API version
|
||||
version = "r"
|
||||
|
||||
@controllers.api_action(
|
||||
method='POST', route='{project_id}/{app}/{route}')
|
||||
async def run(self, request, **kwargs):
|
||||
c = config.Config.config_instance()
|
||||
fnclient = c.functions_client
|
||||
app = request.match_info.get('app')
|
||||
path = "/{}".format(request.match_info.get('route'))
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
# in may appear that no data supplied with POST to function
|
||||
data = {}
|
||||
|
||||
try:
|
||||
fn_app = await fnclient.apps.show(
|
||||
app, loop=c.event_loop)
|
||||
route = await fn_app.routes.show(
|
||||
path, loop=c.event_loop)
|
||||
result = await fn_app.routes.execute(
|
||||
path, loop=c.event_loop, **data)
|
||||
except Exception as ex:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": getattr(ex, "reason", str(ex)),
|
||||
}
|
||||
}, status=getattr(ex, "status", 500))
|
||||
|
||||
def process_result(res):
|
||||
if route.type == "async":
|
||||
_data = {
|
||||
"task_id": res["call_id"],
|
||||
"message": ("App {} async route {} "
|
||||
"execution started".format(app, path))
|
||||
}
|
||||
else:
|
||||
_data = {
|
||||
"result": res,
|
||||
"message": ("App {} sync route {} "
|
||||
"execution finished".format(app, path))
|
||||
}
|
||||
return _data
|
||||
|
||||
return web.json_response(status=200, data=process_result(result))
|
62
laos/api/controllers/tasks.py
Normal file
62
laos/api/controllers/tasks.py
Normal file
@ -0,0 +1,62 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
# from laos.models import app as app_model
|
||||
|
||||
from laos.common.base import controllers
|
||||
# from laos.common import config
|
||||
|
||||
|
||||
# TODO(denismakogon): disabled until
|
||||
# https://github.com/iron-io/functions/issues/275
|
||||
class TasksV1Controller(controllers.ServiceControllerBase):
|
||||
|
||||
controller_name = "tasks"
|
||||
version = "v1"
|
||||
|
||||
@controllers.api_action(
|
||||
method='GET', route='{project_id}/tasks')
|
||||
async def get(self, request, **kwargs):
|
||||
# c = config.Config.config_instance()
|
||||
# fnclient = c.functions_client
|
||||
# project_id = request.match_info.get('project_id')
|
||||
# stored_apps = await app_model.Apps.find_by(project_id=project_id)
|
||||
# final = []
|
||||
# for app in stored_apps:
|
||||
# fn_app = await fnclient.apps.show(app.name, loop=c.event_loop)
|
||||
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "Not supported"
|
||||
}
|
||||
}, status=405)
|
||||
|
||||
@controllers.api_action(
|
||||
method='GET', route='{project_id}/tasks/{task}')
|
||||
async def show(self, request, **kwargs):
|
||||
# c = config.Config.config_instance()
|
||||
# fnclient = c.functions_client
|
||||
# project_id = request.match_info.get('project_id')
|
||||
# stored_apps = await app_model.Apps.find_by(project_id=project_id)
|
||||
# final = []
|
||||
# for app in stored_apps:
|
||||
# fn_app = await fnclient.apps.show(app.name, loop=c.event_loop)
|
||||
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": "Not supported"
|
||||
}
|
||||
}, status=405)
|
0
laos/api/middleware/__init__.py
Normal file
0
laos/api/middleware/__init__.py
Normal file
29
laos/api/middleware/content_type.py
Normal file
29
laos/api/middleware/content_type.py
Normal file
@ -0,0 +1,29 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
async def content_type_validator(app: web.Application, handler):
|
||||
async def middleware_handler(request: web.Request):
|
||||
headers = request.headers
|
||||
content_type = headers.get("Content-Type")
|
||||
if "application/json" not in content_type:
|
||||
return web.json_response(
|
||||
data={
|
||||
"error": {
|
||||
"message": "Invalid content type"
|
||||
}
|
||||
}, status=400)
|
||||
return await handler(request)
|
||||
return middleware_handler
|
46
laos/api/middleware/keystone.py
Normal file
46
laos/api/middleware/keystone.py
Normal file
@ -0,0 +1,46 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from keystoneauth1 import identity
|
||||
from keystoneauth1 import session
|
||||
from keystoneclient import client
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from laos.common import config
|
||||
|
||||
|
||||
async def auth_through_token(app: web.Application, handler):
|
||||
async def middleware_handler(request: web.Request):
|
||||
headers = request.headers
|
||||
x_auth_token = headers.get("X-Auth-Token")
|
||||
project_id = request.match_info.get('project_id')
|
||||
c = config.Config.config_instance()
|
||||
try:
|
||||
auth = identity.Token(c.auth_url,
|
||||
token=x_auth_token,
|
||||
project_id=project_id)
|
||||
sess = session.Session(auth=auth)
|
||||
ks = client.Client(session=sess,
|
||||
project_id=project_id)
|
||||
ks.authenticate(token=x_auth_token)
|
||||
except Exception as ex:
|
||||
return web.json_response(status=401, data={
|
||||
"error": {
|
||||
"message": ("Not authorized. Reason: {}"
|
||||
.format(str(ex)))
|
||||
}
|
||||
})
|
||||
return await handler(request)
|
||||
return middleware_handler
|
0
laos/api/views/__init__.py
Normal file
0
laos/api/views/__init__.py
Normal file
48
laos/api/views/app.py
Normal file
48
laos/api/views/app.py
Normal file
@ -0,0 +1,48 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class AppView(object):
|
||||
|
||||
def __init__(self, stored_app, fn_app):
|
||||
self.app = stored_app
|
||||
self.fn_app = fn_app
|
||||
|
||||
def view(self):
|
||||
return {
|
||||
"id": self.app.id,
|
||||
"name": self.app.name,
|
||||
"config": self.fn_app.config,
|
||||
"description": self.app.description,
|
||||
"created_at": self.app.created_at,
|
||||
"updated_at": self.app.updated_at,
|
||||
"project_id": self.app.project_id,
|
||||
}
|
||||
|
||||
|
||||
class AppRouteView(object):
|
||||
|
||||
def __init__(self, fn_app_routes):
|
||||
self.routes = fn_app_routes
|
||||
|
||||
def view(self):
|
||||
view = []
|
||||
for route in self.routes:
|
||||
view.append({
|
||||
"path": route.path,
|
||||
"type": route.type,
|
||||
"memory": route.memory,
|
||||
"image": route.image,
|
||||
})
|
||||
return view
|
0
laos/common/__init__.py
Normal file
0
laos/common/__init__.py
Normal file
0
laos/common/base/__init__.py
Normal file
0
laos/common/base/__init__.py
Normal file
78
laos/common/base/controllers.py
Normal file
78
laos/common/base/controllers.py
Normal file
@ -0,0 +1,78 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
class ServiceControllerBase(object):
|
||||
|
||||
controller_name = 'abstract'
|
||||
version = ""
|
||||
|
||||
def __get_handlers(self):
|
||||
# when this method gets executed by child classes
|
||||
# method list includes a method of parent class,
|
||||
# so this code ignores it because it doesn't belong to controllers
|
||||
methods = [getattr(self, _m)
|
||||
for _m in dir(self) if inspect.ismethod(
|
||||
getattr(self, _m)) and not _m.startswith("__")][1:]
|
||||
|
||||
return [[method,
|
||||
method.arg_method,
|
||||
method.arg_route] for method in methods]
|
||||
|
||||
def __init__(self, service: web.Application):
|
||||
for fn, http_method, route in self.__get_handlers():
|
||||
proxy_fn = '_'.join([fn.__name__, self.controller_name])
|
||||
setattr(self, proxy_fn, fn)
|
||||
service.router.add_route(http_method,
|
||||
"/{}/{}".format(self.version, route),
|
||||
getattr(self, proxy_fn),
|
||||
name=proxy_fn)
|
||||
|
||||
|
||||
def api_action(**outter_kwargs):
|
||||
"""
|
||||
Wrapps API controller action actions handler
|
||||
:param outter_kwargs: API instance action key-value args
|
||||
:return: _api_handler
|
||||
|
||||
Example:
|
||||
|
||||
class Controller(ControllerBase):
|
||||
|
||||
@api_action(method='GET')
|
||||
async def index(self, request, **kwargs):
|
||||
return web.Response(
|
||||
text=str(request),
|
||||
reason="dumb API",
|
||||
status=201)
|
||||
|
||||
"""
|
||||
|
||||
def _api_handler(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return func(self, *args, *kwargs)
|
||||
|
||||
for key, value in outter_kwargs.items():
|
||||
setattr(wrapper, 'arg_{}'.format(key), value)
|
||||
setattr(wrapper, 'is_module_function', True)
|
||||
return wrapper
|
||||
|
||||
return _api_handler
|
73
laos/common/base/service.py
Normal file
73
laos/common/base/service.py
Normal file
@ -0,0 +1,73 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import typing
|
||||
|
||||
from aiohttp import web
|
||||
from laos.common.base import controllers as c
|
||||
from laos.common import logger as log
|
||||
|
||||
|
||||
class AbstractWebServer(object):
|
||||
|
||||
def __init__(self, host: str='127.0.0.1',
|
||||
port: int= '10001',
|
||||
controllers: typing.List[c.ServiceControllerBase]=None,
|
||||
middlewares: list=None,
|
||||
event_loop: asyncio.AbstractEventLoop=None,
|
||||
logger=log.UnifiedLogger(
|
||||
log_to_console=True,
|
||||
level="INFO").setup_logger(__name__)):
|
||||
"""
|
||||
HTTP server abstraction class
|
||||
:param host:
|
||||
:param port:
|
||||
:param controllers:
|
||||
:param middlewares:
|
||||
:param event_loop:
|
||||
:param logger:
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.controllers = controllers
|
||||
self.event_loop = event_loop
|
||||
self.service = web.Application(
|
||||
loop=self.event_loop,
|
||||
debug=os.environ.get('PYTHONASYNCIODEBUG', 0),
|
||||
middlewares=middlewares if middlewares else [])
|
||||
self.service_handler = None
|
||||
self.server = None
|
||||
self.logger = logger
|
||||
|
||||
def _apply_routers(self):
|
||||
if self.controllers:
|
||||
for controller in self.controllers:
|
||||
controller(self.service)
|
||||
|
||||
def shutdown(self):
|
||||
self.server.close()
|
||||
self.event_loop.run_until_complete(self.server.wait_closed())
|
||||
self.event_loop.run_until_complete(
|
||||
self.service_handler.finish_connections(1.0))
|
||||
self.event_loop.run_until_complete(self.service.cleanup())
|
||||
|
||||
def initialize(self):
|
||||
self._apply_routers()
|
||||
try:
|
||||
web.run_app(self.service, host=self.host, port=self.port,
|
||||
shutdown_timeout=10, access_log=self.logger)
|
||||
except KeyboardInterrupt:
|
||||
self.shutdown()
|
70
laos/common/config.py
Normal file
70
laos/common/config.py
Normal file
@ -0,0 +1,70 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
import aiomysql
|
||||
import asyncio
|
||||
|
||||
from laos.common import utils
|
||||
|
||||
from functionsclient.v1 import client
|
||||
|
||||
|
||||
class Config(object, metaclass=utils.Singleton):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
self.config = self
|
||||
|
||||
@classmethod
|
||||
def config_instance(cls):
|
||||
return cls._instance.config
|
||||
|
||||
|
||||
class Connection(object, metaclass=utils.Singleton):
|
||||
|
||||
def __init__(self, db_uri, loop=None):
|
||||
self.uri = db_uri
|
||||
self.engine = loop.run_until_complete(self.get_engine(loop=loop))
|
||||
self.loop = loop
|
||||
|
||||
def get_engine(self, loop=None):
|
||||
username, password, host, port, db_name = utils.split_db_uri(self.uri)
|
||||
return aiomysql.create_pool(host=host, port=port if port else 3306,
|
||||
user=username, password=password,
|
||||
db=db_name, loop=loop)
|
||||
|
||||
@classmethod
|
||||
def from_class(cls):
|
||||
return cls._instance.engine
|
||||
|
||||
|
||||
class FunctionsClient(client.FunctionsAPIV1, metaclass=utils.Singleton):
|
||||
|
||||
def __init__(self, api_host: str,
|
||||
api_port: int=8080,
|
||||
api_protocol: str="http",
|
||||
api_version: str="v1"):
|
||||
# TODO(denismakogon): enable API version discovery
|
||||
super(FunctionsClient, self).__init__(api_host, api_port, api_protocol)
|
||||
self.client = self
|
||||
|
||||
async def ping(self,
|
||||
loop: asyncio.AbstractEventLoop=asyncio.get_event_loop()):
|
||||
await self.apps.list(loop=loop)
|
||||
|
||||
@classmethod
|
||||
def from_class(cls):
|
||||
return cls._instance.client
|
95
laos/common/logger.py
Normal file
95
laos/common/logger.py
Normal file
@ -0,0 +1,95 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from laos.common import utils
|
||||
|
||||
|
||||
def common_logger_setup(
|
||||
level=logging.DEBUG,
|
||||
filename='/tmp/aiorchestra.log',
|
||||
log_formatter='[%(asctime)s] - '
|
||||
'%(name)s - '
|
||||
'%(levelname)s - '
|
||||
'%(module)s.py:%(lineno)d - '
|
||||
'%(funcName)s - '
|
||||
'%(message)s',
|
||||
datetime_formatter='%Y-%m-%d %H:%M:%S',
|
||||
log_to_console=False):
|
||||
if log_to_console:
|
||||
log_handler = logging.StreamHandler(sys.stdout)
|
||||
else:
|
||||
log_handler = logging.FileHandler(filename)
|
||||
log_format = logging.Formatter(log_formatter, datetime_formatter)
|
||||
log_handler.setFormatter(log_format)
|
||||
return log_handler, level
|
||||
|
||||
|
||||
def setup_logging(name,
|
||||
filename='/tmp/laos-api-{}.log'.format(
|
||||
datetime.datetime.now()),
|
||||
level=logging.DEBUG,
|
||||
log_to_console=False,
|
||||
formatter=None):
|
||||
log_file_handler, log_level = common_logger_setup(
|
||||
filename=filename,
|
||||
level=level,
|
||||
log_to_console=log_to_console,
|
||||
log_formatter=formatter)
|
||||
logger = logging.getLogger(name)
|
||||
logger.addHandler(log_file_handler)
|
||||
logger.setLevel(log_level)
|
||||
return logger
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instance
|
||||
|
||||
|
||||
class UnifiedLogger(object, metaclass=utils.Singleton):
|
||||
|
||||
def __init__(self,
|
||||
filename='/tmp/laos-api-{}.log'.format(
|
||||
datetime.datetime.now()),
|
||||
level=logging.DEBUG, log_to_console=False):
|
||||
self.filename = filename
|
||||
self.level = level
|
||||
if 'DEBUG' not in level:
|
||||
self.log_formatter = (
|
||||
'[%(asctime)s] - '
|
||||
'%(message)s')
|
||||
else:
|
||||
self. log_formatter = (
|
||||
'[%(asctime)s] - '
|
||||
'%(name)s - '
|
||||
'%(levelname)s - '
|
||||
'%(module)s.py:%(lineno)d - '
|
||||
'%(funcName)s - '
|
||||
'%(message)s')
|
||||
self.log_to_console = log_to_console
|
||||
self.logger = self
|
||||
|
||||
def setup_logger(self, name):
|
||||
return setup_logging(name, filename=self.filename,
|
||||
level=self.level,
|
||||
log_to_console=self.log_to_console,
|
||||
formatter=self.log_formatter)
|
102
laos/common/persistence.py
Normal file
102
laos/common/persistence.py
Normal file
@ -0,0 +1,102 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from laos.common import config
|
||||
|
||||
|
||||
class BaseDatabaseModel(object):
|
||||
|
||||
INSERT = "INSERT INTO {} VALUES {}"
|
||||
SELECT = "SELECT * FROM {} {}"
|
||||
WHERE = "WHERE {}"
|
||||
AND = " AND "
|
||||
DELETE = "DELETE FROM {} {}"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = uuid.uuid4().hex
|
||||
self.created_at = str(datetime.datetime.now())
|
||||
self.updated_at = str(datetime.datetime.now())
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
async def save(self):
|
||||
insert = self.INSERT.format(
|
||||
self.table_name,
|
||||
str(tuple([getattr(self, clmn) for clmn in self.columns]))
|
||||
)
|
||||
print(insert)
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(insert)
|
||||
await conn.commit()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
async def delete(cls, **kwargs):
|
||||
delete = cls.DELETE.format(
|
||||
cls.table_name, cls.__define_where(**kwargs))
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(delete)
|
||||
await conn.commit()
|
||||
|
||||
async def update(self, **kwargs):
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute()
|
||||
|
||||
@classmethod
|
||||
async def exists(cls, name, project_id):
|
||||
return True if len(await cls.find_by(
|
||||
name=name,
|
||||
project_id=project_id)) else False
|
||||
|
||||
@classmethod
|
||||
def __define_where(cls, **kwargs):
|
||||
search_field_parts = []
|
||||
final = []
|
||||
for k, v in kwargs.items():
|
||||
search_field_parts.append("{}='{}'".format(k, v))
|
||||
|
||||
for i in range(len(search_field_parts)):
|
||||
final.extend([search_field_parts[i],
|
||||
cls.AND if i != len(
|
||||
search_field_parts) - 1 else ""])
|
||||
|
||||
return cls.WHERE.format("".join(final))
|
||||
|
||||
@classmethod
|
||||
async def find_by(cls, **kwargs):
|
||||
where = cls.__define_where(**kwargs)
|
||||
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(cls.SELECT.format(
|
||||
cls.table_name, where))
|
||||
results = await cur.fetchall()
|
||||
return [cls.from_tuple(instance)
|
||||
for instance in results] if results else []
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, tpl):
|
||||
items = []
|
||||
for i in range(len(tpl)):
|
||||
items.append((cls.columns[i], tpl[i]))
|
||||
return cls(**dict(items))
|
||||
|
||||
def to_dict(self):
|
||||
return self.__dict__
|
42
laos/common/utils.py
Normal file
42
laos/common/utils.py
Normal file
@ -0,0 +1,42 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from urllib import parse
|
||||
|
||||
|
||||
def split_db_uri(db_uri):
|
||||
"""
|
||||
Splits DB URI into consumable parts like:
|
||||
- username
|
||||
- password
|
||||
- hostname
|
||||
- port
|
||||
- protocol schema
|
||||
- path
|
||||
:param db_uri:
|
||||
:return:
|
||||
"""
|
||||
parts = parse.urlparse(db_uri)
|
||||
return (parts.username, parts.password,
|
||||
parts.hostname, parts.port, parts.path[1:])
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
cls._instance = super(
|
||||
Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instance
|
0
laos/models/__init__.py
Normal file
0
laos/models/__init__.py
Normal file
28
laos/models/app.py
Normal file
28
laos/models/app.py
Normal file
@ -0,0 +1,28 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from laos.common import persistence
|
||||
|
||||
|
||||
class Apps(persistence.BaseDatabaseModel):
|
||||
|
||||
table_name = "apps"
|
||||
columns = (
|
||||
"id",
|
||||
"project_id",
|
||||
"description",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name"
|
||||
)
|
0
laos/service/__init__.py
Normal file
0
laos/service/__init__.py
Normal file
121
laos/service/laos_api.py
Normal file
121
laos/service/laos_api.py
Normal file
@ -0,0 +1,121 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import asyncio
|
||||
|
||||
import click
|
||||
import uvloop
|
||||
|
||||
from laos.api.controllers import apps
|
||||
from laos.api.controllers import routes
|
||||
from laos.api.controllers import runnable
|
||||
from laos.api.controllers import tasks
|
||||
|
||||
from laos.api.middleware import content_type
|
||||
from laos.api.middleware import keystone
|
||||
|
||||
from laos.common.base import service
|
||||
from laos.common import config
|
||||
from laos.common import logger as log
|
||||
|
||||
|
||||
class API(service.AbstractWebServer):
|
||||
|
||||
def __init__(self, host: str='0.0.0.0',
|
||||
port: int=10001,
|
||||
loop: asyncio.AbstractEventLoop=asyncio.get_event_loop(),
|
||||
logger=None):
|
||||
super(API, self).__init__(
|
||||
host=host,
|
||||
port=port,
|
||||
controllers=[
|
||||
apps.AppV1Controller,
|
||||
routes.AppRouteV1Controller,
|
||||
runnable.RunnableV1Controller,
|
||||
tasks.TasksV1Controller,
|
||||
],
|
||||
middlewares=[
|
||||
keystone.auth_through_token,
|
||||
content_type.content_type_validator,
|
||||
],
|
||||
event_loop=loop,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
|
||||
@click.command(name='laos-api')
|
||||
@click.option('--host', default='0.0.0.0', help='API service bind host.')
|
||||
@click.option('--port', default=10001, help='API service bind port.')
|
||||
@click.option('--db-uri', default='mysql://root:root@localhost/functions',
|
||||
help='LaOS persistence storage URI.')
|
||||
@click.option('--keystone-endpoint', default='http://localhost:5000/v3',
|
||||
help='OpenStack Identity service endpoint.')
|
||||
@click.option('--functions-host', default='localhost',
|
||||
help='Functions API host')
|
||||
@click.option('--functions-port', default=10501,
|
||||
help='Functions API port')
|
||||
@click.option('--functions-api-version', default='v1',
|
||||
help='Functions API version')
|
||||
@click.option('--functions-api-protocol', default='http',
|
||||
help='Functions API protocol')
|
||||
@click.option('--log-level', default='INFO',
|
||||
help='Logging file')
|
||||
@click.option('--log-file', default=None,
|
||||
help='Log file path')
|
||||
def server(host, port, db_uri,
|
||||
keystone_endpoint,
|
||||
functions_host,
|
||||
functions_port,
|
||||
functions_api_version,
|
||||
functions_api_protocol,
|
||||
log_level,
|
||||
log_file,
|
||||
):
|
||||
"""
|
||||
Starts an Project Laos API service
|
||||
"""
|
||||
logger = log.UnifiedLogger(
|
||||
log_to_console=True if not log_file else False,
|
||||
filename=None if not log_file else log_file,
|
||||
level=log_level).setup_logger(__name__)
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
fnclient = config.FunctionsClient(
|
||||
functions_host,
|
||||
api_port=functions_port,
|
||||
api_protocol=functions_api_protocol,
|
||||
api_version=functions_api_version,
|
||||
)
|
||||
loop.run_until_complete(fnclient.ping(loop=loop))
|
||||
conn = config.Connection(db_uri, loop=loop)
|
||||
|
||||
config.Config(
|
||||
auth_url=keystone_endpoint,
|
||||
functions_host=functions_host,
|
||||
functions_port=functions_port,
|
||||
functions_api_protocol=functions_api_protocol,
|
||||
functions_api_version=functions_api_version,
|
||||
functions_client=fnclient,
|
||||
logger=logger,
|
||||
connection=conn,
|
||||
event_loop=loop,
|
||||
)
|
||||
|
||||
API(host=host, port=port, loop=loop, logger=logger).initialize()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server()
|
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
70
migrations/env.py
Normal file
70
migrations/env.py
Normal file
@ -0,0 +1,70 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
33
migrations/versions/7a2dcf8ac8bf_.py
Normal file
33
migrations/versions/7a2dcf8ac8bf_.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 7a2dcf8ac8bf
|
||||
Revises:
|
||||
Create Date: 2016-11-9 16:24:27.886139
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7a2dcf8ac8bf'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'apps',
|
||||
sa.Column('id', sa.String(255), nullable=False),
|
||||
sa.Column('project_id', sa.String(255), nullable=False),
|
||||
sa.Column('description', sa.String(255), nullable=True),
|
||||
sa.Column('created_at', sa.String(255)),
|
||||
sa.Column('updated_at', sa.String(255)),
|
||||
sa.Column('name', sa.String(255), nullable=False, primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('apps')
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
uvloop
|
||||
aiohttp # Apache-2.0
|
||||
aiomysql
|
||||
alembic>=0.8.4 # MIT
|
||||
click
|
||||
keystoneauth1>=2.14.0 # Apache-2.0
|
||||
python-keystoneclient==3.6.0
|
||||
SQLAlchemy<1.1.0,>=1.0.10 # MIT
|
76
setup.py
Normal file
76
setup.py
Normal file
@ -0,0 +1,76 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import setuptools
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
setuptools.setup(
|
||||
name='laos',
|
||||
version='0.0.1',
|
||||
description='Project LaOS (Lambdas-on-OpenStack',
|
||||
long_description=read('README.md'),
|
||||
url='laos.readthedocs.org',
|
||||
author='Denis Makogon',
|
||||
author_email='denis@iron.io',
|
||||
packages=setuptools.find_packages(),
|
||||
install_requires=[
|
||||
"uvloop",
|
||||
"aiohttp",
|
||||
"aiomysql",
|
||||
"alembic>=0.8.4",
|
||||
"click",
|
||||
"keystoneauth1>=2.14.0",
|
||||
"python-keystoneclient==3.6.0",
|
||||
"SQLAlchemy<1.1.0,>=1.0.10",
|
||||
],
|
||||
license='License :: OSI Approved :: Apache Software License',
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Intended Audience :: Developers',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: Software Development :: '
|
||||
'Libraries :: Python Modules',
|
||||
'Topic :: System :: Distributed Computing',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: Unix',
|
||||
'Operating System :: MacOS',
|
||||
],
|
||||
keywords=['functions', 'lambdas', 'python API'],
|
||||
platforms=['Linux', 'Mac OS-X', 'Unix'],
|
||||
tests_require=[
|
||||
'flake8==2.5.0',
|
||||
'hacking<0.11,>=0.10.0',
|
||||
'coverage>=4.0',
|
||||
'sphinx!=1.3b1,<1.4,>=1.2.1',
|
||||
'testrepository>=0.0.18',
|
||||
'testtools>=1.4.0',
|
||||
],
|
||||
zip_safe=True,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'laos-api = laos.service.laos_api:server',
|
||||
]
|
||||
},
|
||||
|
||||
)
|
10
test-requirements.txt
Normal file
10
test-requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
flake8==2.5.0 # fix for https://gitlab.com/pycqa/flake8/issues/94
|
||||
hacking<0.11,>=0.10.0
|
||||
coverage>=4.0 # Apache-2.0
|
||||
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
44
tox.ini
Normal file
44
tox.ini
Normal file
@ -0,0 +1,44 @@
|
||||
# Function Python client
|
||||
|
||||
[tox]
|
||||
envlist = py35,pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
passenv =
|
||||
PYTHONASYNCIODEBUG
|
||||
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = find . -type f -name "*.pyc" -delete
|
||||
rm -f .testrepository/times.dbm
|
||||
python setup.py testr --testr-args='{posargs}'
|
||||
whitelist_externals = find
|
||||
rm
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands =
|
||||
coverage erase
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
[testenv:docs]
|
||||
commands =
|
||||
rm -rf doc/html doc/build
|
||||
python setup.py build_sphinx
|
||||
|
||||
[flake8]
|
||||
ignore = H202,H404,H405,H501
|
||||
show-source = True
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,migrations
|
Loading…
Reference in New Issue
Block a user