summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiroyuki Eguchi <h-eguchi@az.jp.nec.com>2016-12-08 12:33:50 +0000
committerHiroyuki Eguchi <h-eguchi@az.jp.nec.com>2016-12-08 12:33:50 +0000
commit1491d54a1d36c3cb751520b28e739a0059befce5 (patch)
tree304c6a8a22ed844ca2541baf7f20b32ef67182f4
parent5b9c316aa5387bb325e1ba9e7cbfde9bd786cf58 (diff)
Initial commit
Notes
Notes (review): Code-Review+2: Hiroyuki Eguchi <h-eguchi@az.jp.nec.com> Workflow+1: Hiroyuki Eguchi <h-eguchi@az.jp.nec.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Thu, 08 Dec 2016 12:34:49 +0000 Reviewed-on: https://review.openstack.org/408588 Project: openstack/meteos Branch: refs/heads/master
-rw-r--r--CONTRIBUTING.rst19
-rw-r--r--HACKING.rst11
-rw-r--r--LICENSE176
-rw-r--r--README.rst27
-rw-r--r--babel.cfg2
-rw-r--r--devstack/README.rst21
-rw-r--r--devstack/exercise.sh50
-rw-r--r--devstack/plugin.sh187
-rw-r--r--devstack/settings37
-rw-r--r--etc/meteos/README.meteos.conf4
-rw-r--r--etc/meteos/api-paste.ini49
-rw-r--r--etc/meteos/api-paste.ini.orig49
-rw-r--r--etc/meteos/logging_sample.conf73
-rw-r--r--etc/meteos/meteos.conf39
-rw-r--r--etc/meteos/policy.json13
-rw-r--r--etc/meteos/rootwrap.conf27
-rw-r--r--etc/meteos/rootwrap.d/learning.filters20
-rw-r--r--etc/oslo-config-generator/meteos.conf9
-rw-r--r--meteos/__init__.py0
-rw-r--r--meteos/api/__init__.py21
-rw-r--r--meteos/api/auth.py38
-rw-r--r--meteos/api/common.py318
-rw-r--r--meteos/api/contrib/__init__.py37
-rw-r--r--meteos/api/middleware/__init__.py0
-rw-r--r--meteos/api/middleware/auth.py150
-rw-r--r--meteos/api/middleware/fault.py74
-rw-r--r--meteos/api/openstack/__init__.py81
-rw-r--r--meteos/api/openstack/api_version_request.py169
-rw-r--r--meteos/api/openstack/rest_api_version_history.rst14
-rw-r--r--meteos/api/openstack/urlmap.py29
-rw-r--r--meteos/api/openstack/versioned_method.py49
-rw-r--r--meteos/api/openstack/wsgi.py1343
-rw-r--r--meteos/api/urlmap.py291
-rw-r--r--meteos/api/v1/__init__.py0
-rw-r--r--meteos/api/v1/datasets.py156
-rw-r--r--meteos/api/v1/experiments.py144
-rw-r--r--meteos/api/v1/learnings.py155
-rw-r--r--meteos/api/v1/models.py158
-rw-r--r--meteos/api/v1/router.py73
-rw-r--r--meteos/api/v1/templates.py145
-rw-r--r--meteos/api/versions.py97
-rw-r--r--meteos/api/views/__init__.py0
-rw-r--r--meteos/api/views/datasets.py80
-rw-r--r--meteos/api/views/experiments.py80
-rw-r--r--meteos/api/views/learnings.py84
-rw-r--r--meteos/api/views/models.py82
-rw-r--r--meteos/api/views/templates.py85
-rw-r--r--meteos/api/views/versions.py66
-rw-r--r--meteos/cluster/__init__.py33
-rw-r--r--meteos/cluster/binary/meteos-script-1.6.0.py386
-rw-r--r--meteos/cluster/sahara.py177
-rw-r--r--meteos/cmd/__init__.py0
-rw-r--r--meteos/cmd/api.py54
-rw-r--r--meteos/cmd/engine.py60
-rw-r--r--meteos/cmd/manage.py465
-rw-r--r--meteos/common/__init__.py0
-rw-r--r--meteos/common/client_auth.py107
-rw-r--r--meteos/common/config.py178
-rw-r--r--meteos/common/constants.py28
-rw-r--r--meteos/context.py151
-rw-r--r--meteos/db/__init__.py20
-rw-r--r--meteos/db/api.py328
-rw-r--r--meteos/db/base.py37
-rw-r--r--meteos/db/migration.py48
-rw-r--r--meteos/db/migrations/__init__.py0
-rw-r--r--meteos/db/migrations/alembic.ini59
-rw-r--r--meteos/db/migrations/alembic/__init__.py0
-rw-r--r--meteos/db/migrations/alembic/env.py41
-rw-r--r--meteos/db/migrations/alembic/migration.py84
-rw-r--r--meteos/db/migrations/alembic/script.py.mako34
-rw-r--r--meteos/db/migrations/alembic/versions/001_meteos_init.py228
-rw-r--r--meteos/db/migrations/utils.py21
-rw-r--r--meteos/db/sqlalchemy/__init__.py0
-rw-r--r--meteos/db/sqlalchemy/api.py915
-rw-r--r--meteos/db/sqlalchemy/models.py200
-rw-r--r--meteos/db/sqlalchemy/query.py40
-rw-r--r--meteos/engine/__init__.py25
-rw-r--r--meteos/engine/api.py470
-rw-r--r--meteos/engine/configuration.py81
-rw-r--r--meteos/engine/driver.py127
-rw-r--r--meteos/engine/drivers/__init__.py22
-rw-r--r--meteos/engine/drivers/generic.py468
-rw-r--r--meteos/engine/manager.py340
-rw-r--r--meteos/engine/rpcapi.py118
-rw-r--r--meteos/exception.py194
-rw-r--r--meteos/hacking/__init__.py0
-rw-r--r--meteos/hacking/checks.py360
-rw-r--r--meteos/i18n.py50
-rw-r--r--meteos/manager.py114
-rw-r--r--meteos/opts.py75
-rw-r--r--meteos/policy.py113
-rw-r--r--meteos/rpc.py153
-rw-r--r--meteos/service.py379
-rw-r--r--meteos/test.py354
-rw-r--r--meteos/testing/README.rst53
-rw-r--r--meteos/tests/__init__.py27
-rw-r--r--meteos/utils.py400
-rw-r--r--meteos/version.py23
-rw-r--r--meteos/wsgi.py551
-rw-r--r--pylintrc38
-rw-r--r--releasenotes/notes/.placeholder0
-rw-r--r--requirements.txt45
-rw-r--r--setup.cfg67
-rw-r--r--setup.py29
-rw-r--r--test-requirements.txt32
-rw-r--r--tox.ini106
106 files changed, 13340 insertions, 0 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..81d0448
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,19 @@
1If you would like to contribute to the development of OpenStack,
2you must follow the steps in this page:
3
4 http://docs.openstack.org/infra/manual/developers.html
5
6Once those steps have been completed, changes to OpenStack
7should be submitted for review via the Gerrit tool, following
8the workflow documented at:
9
10 http://docs.openstack.org/infra/manual/developers.html#development-workflow
11
12Pull requests submitted through GitHub will be ignored.
13
14Bugs should be filed on Launchpad, not GitHub:
15
16 https://bugs.launchpad.net/meteos
17
18
19
diff --git a/HACKING.rst b/HACKING.rst
new file mode 100644
index 0000000..c3990d0
--- /dev/null
+++ b/HACKING.rst
@@ -0,0 +1,11 @@
1Meteos Style Commandments
2============================
3
4- Step 1: Read the OpenStack Style Commandments
5 http://docs.openstack.org/developer/hacking/
6- Step 2: Read on
7
8
9Meteos Specific Commandments
10-------------------------------
11
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..68c771a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,176 @@
1
2 Apache License
3 Version 2.0, January 2004
4 http://www.apache.org/licenses/
5
6 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8 1. Definitions.
9
10 "License" shall mean the terms and conditions for use, reproduction,
11 and distribution as defined by Sections 1 through 9 of this document.
12
13 "Licensor" shall mean the copyright owner or entity authorized by
14 the copyright owner that is granting the License.
15
16 "Legal Entity" shall mean the union of the acting entity and all
17 other entities that control, are controlled by, or are under common
18 control with that entity. For the purposes of this definition,
19 "control" means (i) the power, direct or indirect, to cause the
20 direction or management of such entity, whether by contract or
21 otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 outstanding shares, or (iii) beneficial ownership of such entity.
23
24 "You" (or "Your") shall mean an individual or Legal Entity
25 exercising permissions granted by this License.
26
27 "Source" form shall mean the preferred form for making modifications,
28 including but not limited to software source code, documentation
29 source, and configuration files.
30
31 "Object" form shall mean any form resulting from mechanical
32 transformation or translation of a Source form, including but
33 not limited to compiled object code, generated documentation,
34 and conversions to other media types.
35
36 "Work" shall mean the work of authorship, whether in Source or
37 Object form, made available under the License, as indicated by a
38 copyright notice that is included in or attached to the work
39 (an example is provided in the Appendix below).
40
41 "Derivative Works" shall mean any work, whether in Source or Object
42 form, that is based on (or derived from) the Work and for which the
43 editorial revisions, annotations, elaborations, or other modifications
44 represent, as a whole, an original work of authorship. For the purposes
45 of this License, Derivative Works shall not include works that remain
46 separable from, or merely link (or bind by name) to the interfaces of,
47 the Work and Derivative Works thereof.
48
49 "Contribution" shall mean any work of authorship, including
50 the original version of the Work and any modifications or additions
51 to that Work or Derivative Works thereof, that is intentionally
52 submitted to Licensor for inclusion in the Work by the copyright owner
53 or by an individual or Legal Entity authorized to submit on behalf of
54 the copyright owner. For the purposes of this definition, "submitted"
55 means any form of electronic, verbal, or written communication sent
56 to the Licensor or its representatives, including but not limited to
57 communication on electronic mailing lists, source code control systems,
58 and issue tracking systems that are managed by, or on behalf of, the
59 Licensor for the purpose of discussing and improving the Work, but
60 excluding communication that is conspicuously marked or otherwise
61 designated in writing by the copyright owner as "Not a Contribution."
62
63 "Contributor" shall mean Licensor and any individual or Legal Entity
64 on behalf of whom a Contribution has been received by Licensor and
65 subsequently incorporated within the Work.
66
67 2. Grant of Copyright License. Subject to the terms and conditions of
68 this License, each Contributor hereby grants to You a perpetual,
69 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 copyright license to reproduce, prepare Derivative Works of,
71 publicly display, publicly perform, sublicense, and distribute the
72 Work and such Derivative Works in Source or Object form.
73
74 3. Grant of Patent License. Subject to the terms and conditions of
75 this License, each Contributor hereby grants to You a perpetual,
76 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 (except as stated in this section) patent license to make, have made,
78 use, offer to sell, sell, import, and otherwise transfer the Work,
79 where such license applies only to those patent claims licensable
80 by such Contributor that are necessarily infringed by their
81 Contribution(s) alone or by combination of their Contribution(s)
82 with the Work to which such Contribution(s) was submitted. If You
83 institute patent litigation against any entity (including a
84 cross-claim or counterclaim in a lawsuit) alleging that the Work
85 or a Contribution incorporated within the Work constitutes direct
86 or contributory patent infringement, then any patent licenses
87 granted to You under this License for that Work shall terminate
88 as of the date such litigation is filed.
89
90 4. Redistribution. You may reproduce and distribute copies of the
91 Work or Derivative Works thereof in any medium, with or without
92 modifications, and in Source or Object form, provided that You
93 meet the following conditions:
94
95 (a) You must give any other recipients of the Work or
96 Derivative Works a copy of this License; and
97
98 (b) You must cause any modified files to carry prominent notices
99 stating that You changed the files; and
100
101 (c) You must retain, in the Source form of any Derivative Works
102 that You distribute, all copyright, patent, trademark, and
103 attribution notices from the Source form of the Work,
104 excluding those notices that do not pertain to any part of
105 the Derivative Works; and
106
107 (d) If the Work includes a "NOTICE" text file as part of its
108 distribution, then any Derivative Works that You distribute must
109 include a readable copy of the attribution notices contained
110 within such NOTICE file, excluding those notices that do not
111 pertain to any part of the Derivative Works, in at least one
112 of the following places: within a NOTICE text file distributed
113 as part of the Derivative Works; within the Source form or
114 documentation, if provided along with the Derivative Works; or,
115 within a display generated by the Derivative Works, if and
116 wherever such third-party notices normally appear. The contents
117 of the NOTICE file are for informational purposes only and
118 do not modify the License. You may add Your own attribution
119 notices within Derivative Works that You distribute, alongside
120 or as an addendum to the NOTICE text from the Work, provided
121 that such additional attribution notices cannot be construed
122 as modifying the License.
123
124 You may add Your own copyright statement to Your modifications and
125 may provide additional or different license terms and conditions
126 for use, reproduction, or distribution of Your modifications, or
127 for any such Derivative Works as a whole, provided Your use,
128 reproduction, and distribution of the Work otherwise complies with
129 the conditions stated in this License.
130
131 5. Submission of Contributions. Unless You explicitly state otherwise,
132 any Contribution intentionally submitted for inclusion in the Work
133 by You to the Licensor shall be under the terms and conditions of
134 this License, without any additional terms or conditions.
135 Notwithstanding the above, nothing herein shall supersede or modify
136 the terms of any separate license agreement you may have executed
137 with Licensor regarding such Contributions.
138
139 6. Trademarks. This License does not grant permission to use the trade
140 names, trademarks, service marks, or product names of the Licensor,
141 except as required for reasonable and customary use in describing the
142 origin of the Work and reproducing the content of the NOTICE file.
143
144 7. Disclaimer of Warranty. Unless required by applicable law or
145 agreed to in writing, Licensor provides the Work (and each
146 Contributor provides its Contributions) on an "AS IS" BASIS,
147 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 implied, including, without limitation, any warranties or conditions
149 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 PARTICULAR PURPOSE. You are solely responsible for determining the
151 appropriateness of using or redistributing the Work and assume any
152 risks associated with Your exercise of permissions under this License.
153
154 8. Limitation of Liability. In no event and under no legal theory,
155 whether in tort (including negligence), contract, or otherwise,
156 unless required by applicable law (such as deliberate and grossly
157 negligent acts) or agreed to in writing, shall any Contributor be
158 liable to You for damages, including any direct, indirect, special,
159 incidental, or consequential damages of any character arising as a
160 result of this License or out of the use or inability to use the
161 Work (including but not limited to damages for loss of goodwill,
162 work stoppage, computer failure or malfunction, or any and all
163 other commercial damages or losses), even if such Contributor
164 has been advised of the possibility of such damages.
165
166 9. Accepting Warranty or Additional Liability. While redistributing
167 the Work or Derivative Works thereof, You may choose to offer,
168 and charge a fee for, acceptance of support, warranty, indemnity,
169 or other liability obligations and/or rights consistent with this
170 License. However, in accepting such obligations, You may act only
171 on Your own behalf and on Your sole responsibility, not on behalf
172 of any other Contributor, and only if You agree to indemnify,
173 defend, and hold each Contributor harmless for any liability
174 incurred by, or claims asserted against, such Contributor by reason
175 of your accepting any such warranty or additional liability.
176
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..4f7f984
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,27 @@
1======
2Meteos
3======
4
5You have come across an OpenStack Machine Learning service. It has
6identified itself as "Meteos." It was abstracted from the Manila
7project.
8
9* Wiki: https://wiki.openstack.org/Meteos
10* Developer docs: http://docs.openstack.org/developer/meteos
11
12Getting Started
13---------------
14
15If you'd like to run from the master branch, you can clone the git repo:
16
17 git clone https://github.com/openstack/meteos.git
18
19For developer information please see
20`HACKING.rst <https://github.com/openstack/meteos/blob/master/HACKING.rst>`_
21
22You can raise bugs here http://bugs.launchpad.net/meteos
23
24Python client
25-------------
26
27https://github.com/openstack/python-meteosclient.git
diff --git a/babel.cfg b/babel.cfg
new file mode 100644
index 0000000..15cd6cb
--- /dev/null
+++ b/babel.cfg
@@ -0,0 +1,2 @@
1[python: **.py]
2
diff --git a/devstack/README.rst b/devstack/README.rst
new file mode 100644
index 0000000..21aca85
--- /dev/null
+++ b/devstack/README.rst
@@ -0,0 +1,21 @@
1======================
2 Enabling in Devstack
3======================
4
51. Download DevStack
6
72. Add this repo as an external repository in ``local.conf``
8
9.. sourcecode:: bash
10
11 [[local|localrc]]
12 enable_plugin meteos git://git.openstack.org/openstack/meteos
13
14Optionally, a git refspec may be provided as follows:
15
16.. sourcecode:: bash
17
18 [[local|localrc]]
19 enable_plugin meteos git://git.openstack.org/openstack/meteos <refspec>
20
213. run ``stack.sh``
diff --git a/devstack/exercise.sh b/devstack/exercise.sh
new file mode 100644
index 0000000..ef4794b
--- /dev/null
+++ b/devstack/exercise.sh
@@ -0,0 +1,50 @@
1#!/usr/bin/env bash
2
3# Sanity check that Meteos started if enabled
4
5echo "*********************************************************************"
6echo "Begin DevStack Exercise: $0"
7echo "*********************************************************************"
8
9# This script exits on an error so that errors don't compound and you see
10# only the first error that occurred.
11set -o errexit
12
13# Print the commands being run so that we can see the command that triggers
14# an error. It is also useful for following allowing as the install occurs.
15set -o xtrace
16
17
18# Settings
19# ========
20
21# Keep track of the current directory
22EXERCISE_DIR=$(cd $(dirname "$0") && pwd)
23TOP_DIR=$(cd $EXERCISE_DIR/..; pwd)
24
25# Import common functions
26source $TOP_DIR/functions
27
28# Import configuration
29source $TOP_DIR/openrc
30
31# Import exercise configuration
32source $TOP_DIR/exerciserc
33
34is_service_enabled meteos || exit 55
35
36if is_ssl_enabled_service "meteos" ||\
37 is_ssl_enabled_service "meteos-api" ||\
38 is_service_enabled tls-proxy; then
39 METEOS_SERVICE_PROTOCOL="https"
40fi
41
42METEOS_SERVICE_PROTOCOL=${METEOS_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
43
44$CURL_GET $METEOS_SERVICE_PROTOCOL://$SERVICE_HOST:8989/ 2>/dev/null \
45 | grep -q 'Auth' || die $LINENO "Meteos API isn't functioning!"
46
47set +o xtrace
48echo "*********************************************************************"
49echo "SUCCESS: End DevStack Exercise: $0"
50echo "*********************************************************************"
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
new file mode 100644
index 0000000..9624055
--- /dev/null
+++ b/devstack/plugin.sh
@@ -0,0 +1,187 @@
1#!/bin/bash
2#
3# lib/meteos
4
5# Dependencies:
6# ``functions`` file
7# ``DEST``, ``DATA_DIR``, ``STACK_USER`` must be defined
8
9# ``stack.sh`` calls the entry points in this order:
10#
11# install_meteos
12# install_python_meteosclient
13# configure_meteos
14# start_meteos
15# stop_meteos
16# cleanup_meteos
17
18# Save trace setting
19XTRACE=$(set +o | grep xtrace)
20set +o xtrace
21
22
23# Functions
24# ---------
25
26# create_meteos_accounts() - Set up common required meteos accounts
27#
28# Tenant User Roles
29# ------------------------------
30# service meteos admin
31function create_meteos_accounts {
32
33 create_service_user "meteos"
34
35 get_or_create_service "meteos" "machine-learning" "Meteos Machine Learning"
36 get_or_create_endpoint "machine-learning" \
37 "$REGION_NAME" \
38 "$METEOS_SERVICE_PROTOCOL://$METEOS_SERVICE_HOST:$METEOS_SERVICE_PORT/v1/\$(tenant_id)s" \
39 "$METEOS_SERVICE_PROTOCOL://$METEOS_SERVICE_HOST:$METEOS_SERVICE_PORT/v1/\$(tenant_id)s" \
40 "$METEOS_SERVICE_PROTOCOL://$METEOS_SERVICE_HOST:$METEOS_SERVICE_PORT/v1/\$(tenant_id)s"
41}
42
43# cleanup_meteos() - Remove residual data files, anything left over from
44# previous runs that would need to clean up.
45function cleanup_meteos {
46
47 # Cleanup auth cache dir
48 sudo rm -rf $METEOS_AUTH_CACHE_DIR
49}
50
51# configure_meteos() - Set config files, create data dirs, etc
52function configure_meteos {
53 sudo install -d -o $STACK_USER $METEOS_CONF_DIR
54
55 if [[ -f $METEOS_DIR/etc/meteos/policy.json ]]; then
56 cp -p $METEOS_DIR/etc/meteos/policy.json $METEOS_CONF_DIR
57 fi
58
59 cp -p $METEOS_DIR/etc/meteos/api-paste.ini $METEOS_CONF_DIR
60
61 # Create auth cache dir
62 sudo install -d -o $STACK_USER -m 700 $METEOS_AUTH_CACHE_DIR
63 rm -rf $METEOS_AUTH_CACHE_DIR/*
64
65 configure_auth_token_middleware \
66 $METEOS_CONF_FILE meteos $METEOS_AUTH_CACHE_DIR
67
68 # Set admin user parameters needed for trusts creation
69 iniset $METEOS_CONF_FILE \
70 keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME
71 iniset $METEOS_CONF_FILE keystone_authtoken admin_user meteos
72 iniset $METEOS_CONF_FILE \
73 keystone_authtoken admin_password $SERVICE_PASSWORD
74
75 iniset_rpc_backend meteos $METEOS_CONF_FILE DEFAULT
76
77 # Set configuration to send notifications
78 iniset $METEOS_CONF_FILE DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
79
80 iniset $METEOS_CONF_FILE DEFAULT plugins $METEOS_ENABLED_PLUGINS
81
82 iniset $METEOS_CONF_FILE \
83 database connection `database_connection_url meteos`
84
85 # Format logging
86 if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ]; then
87 setup_colorized_logging $METEOS_CONF_FILE DEFAULT
88 fi
89
90 recreate_database meteos
91 $METEOS_BIN_DIR/meteos-manage \
92 --config-file $METEOS_CONF_FILE db sync
93}
94
95# install_meteos() - Collect source and prepare
96function install_meteos {
97 setup_develop $METEOS_DIR
98}
99
100# install_python_meteosclient() - Collect source and prepare
101function install_python_meteosclient {
102 git_clone $METEOSCLIENT_REPO $METEOSCLIENT_DIR $METEOSCLIENT_BRANCH
103 setup_develop $METEOSCLIENT_DIR
104}
105
106# start_meteos() - Start running processes, including screen
107function start_meteos {
108 local service_port=$METEOS_SERVICE_PORT
109 local service_protocol=$METEOS_SERVICE_PROTOCOL
110
111 run_process meteos-all "$METEOS_BIN_DIR/meteos-all \
112 --config-file $METEOS_CONF_FILE"
113 run_process meteos-api "$METEOS_BIN_DIR/meteos-api \
114 --config-file $METEOS_CONF_FILE"
115 run_process meteos-eng "$METEOS_BIN_DIR/meteos-engine \
116 --config-file $METEOS_CONF_FILE"
117
118 echo "Waiting for Meteos to start..."
119 if ! wait_for_service $SERVICE_TIMEOUT \
120 $service_protocol://$METEOS_SERVICE_HOST:$service_port; then
121 die $LINENO "Meteos did not start"
122 fi
123}
124
125# configure_tempest_for_meteos() - Tune Tempest configuration for Meteos
126function configure_tempest_for_meteos {
127 if is_service_enabled tempest; then
128 iniset $TEMPEST_CONFIG service_available meteos True
129 iniset $TEMPEST_CONFIG data-processing-feature-enabled plugins $METEOS_ENABLED_PLUGINS
130 fi
131}
132
133# stop_meteos() - Stop running processes
134function stop_meteos {
135 # Kill the Meteos screen windows
136 stop_process meteos-all
137 stop_process meteos-api
138 stop_process meteos-eng
139}
140
141# is_meteos_enabled. This allows is_service_enabled meteos work
142# correctly throughout devstack.
143function is_meteos_enabled {
144 if is_service_enabled meteos-api || \
145 is_service_enabled meteos-eng || \
146 is_service_enabled meteos-all; then
147 return 0
148 else
149 return 1
150 fi
151}
152
153# Dispatcher for Meteos plugin
154if is_service_enabled meteos; then
155 if [[ "$1" == "stack" && "$2" == "install" ]]; then
156 echo_summary "Installing meteos"
157 install_meteos
158 install_python_meteosclient
159 cleanup_meteos
160 elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
161 echo_summary "Configuring meteos"
162 configure_meteos
163 create_meteos_accounts
164 elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
165 echo_summary "Initializing meteos"
166 start_meteos
167 elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
168 echo_summary "Configuring tempest"
169 configure_tempest_for_meteos
170 fi
171
172 if [[ "$1" == "unstack" ]]; then
173 stop_meteos
174 fi
175
176 if [[ "$1" == "clean" ]]; then
177 cleanup_meteos
178 fi
179fi
180
181
182# Restore xtrace
183$XTRACE
184
185# Local variables:
186# mode: shell-script
187# End:
diff --git a/devstack/settings b/devstack/settings
new file mode 100644
index 0000000..9d6203f
--- /dev/null
+++ b/devstack/settings
@@ -0,0 +1,37 @@
1#!/bin/bash
2
3# Settings needed for the Meteos plugin
4# -------------------------------------
5
6# Set up default directories
7METEOSCLIENT_DIR=$DEST/python-meteosclient
8METEOS_DIR=$DEST/meteos
9
10METEOSCLIENT_REPO=${METEOSCLIENT_REPO:-\
11${GIT_BASE}/openstack/python-meteosclient.git}
12METEOSCLIENT_BRANCH=${METEOSCLIENT_BRANCH:-master}
13
14METEOS_CONF_DIR=${METEOS_CONF_DIR:-/etc/meteos}
15METEOS_CONF_FILE=${METEOS_CONF_DIR}/meteos.conf
16
17# TODO(slukjanov): Should we append meteos to SSL_ENABLED_SERVICES?
18
19if is_ssl_enabled_service "meteos" || is_service_enabled tls-proxy; then
20 METEOS_SERVICE_PROTOCOL="https"
21fi
22METEOS_SERVICE_HOST=${METEOS_SERVICE_HOST:-$SERVICE_HOST}
23METEOS_SERVICE_PORT=${METEOS_SERVICE_PORT:-8989}
24METEOS_SERVICE_PROTOCOL=${METEOS_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
25METEOS_ENDPOINT_TYPE=${METEOS_ENDPOINT_TYPE:-adminURL}
26
27METEOS_AUTH_CACHE_DIR=${METEOS_AUTH_CACHE_DIR:-/var/cache/meteos}
28
29METEOS_BIN_DIR=$(get_python_exec_prefix)
30
31METEOS_ENABLE_DISTRIBUTED_PERIODICS=${METEOS_ENABLE_DISTRIBUTED_PERIODICS:-\
32True}
33
34# Tell Tempest this project is present
35TEMPEST_SERVICES+=,meteos
36
37enable_service meteos-api meteos-eng
diff --git a/etc/meteos/README.meteos.conf b/etc/meteos/README.meteos.conf
new file mode 100644
index 0000000..a4a7d31
--- /dev/null
+++ b/etc/meteos/README.meteos.conf
@@ -0,0 +1,4 @@
1To generate the sample meteos.conf file, run the following
2command from the top level of the meteos directory:
3
4tox -egenconfig
diff --git a/etc/meteos/api-paste.ini b/etc/meteos/api-paste.ini
new file mode 100644
index 0000000..574527d
--- /dev/null
+++ b/etc/meteos/api-paste.ini
@@ -0,0 +1,49 @@
1#############
2# OpenStack #
3#############
4
5[composite:osapi_learning]
6use = call:meteos.api:root_app_factory
7/: apiversions
8/v1: openstack_learning_api
9
10[composite:openstack_learning_api]
11use = call:meteos.api.middleware.auth:pipeline_factory
12noauth = cors faultwrap http_proxy_to_wsgi sizelimit noauth api
13keystone = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
14keystone_nolimit = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
15
16[filter:faultwrap]
17paste.filter_factory = meteos.api.middleware.fault:FaultWrapper.factory
18
19[filter:noauth]
20paste.filter_factory = meteos.api.middleware.auth:NoAuthMiddleware.factory
21
22[filter:sizelimit]
23paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
24
25[filter:http_proxy_to_wsgi]
26paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
27
28[app:api]
29paste.app_factory = meteos.api.v1.router:APIRouter.factory
30
31[pipeline:apiversions]
32pipeline = cors faultwrap http_proxy_to_wsgi oslearningversionapp
33
34[app:oslearningversionapp]
35paste.app_factory = meteos.api.versions:VersionsRouter.factory
36
37##########
38# Engine #
39##########
40
41[filter:keystonecontext]
42paste.filter_factory = meteos.api.middleware.auth:MeteosKeystoneContext.factory
43
44[filter:authtoken]
45paste.filter_factory = keystonemiddleware.auth_token:filter_factory
46
47[filter:cors]
48paste.filter_factory = oslo_middleware.cors:filter_factory
49oslo_config_project = meteos
diff --git a/etc/meteos/api-paste.ini.orig b/etc/meteos/api-paste.ini.orig
new file mode 100644
index 0000000..574527d
--- /dev/null
+++ b/etc/meteos/api-paste.ini.orig
@@ -0,0 +1,49 @@
1#############
2# OpenStack #
3#############
4
5[composite:osapi_learning]
6use = call:meteos.api:root_app_factory
7/: apiversions
8/v1: openstack_learning_api
9
10[composite:openstack_learning_api]
11use = call:meteos.api.middleware.auth:pipeline_factory
12noauth = cors faultwrap http_proxy_to_wsgi sizelimit noauth api
13keystone = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
14keystone_nolimit = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
15
16[filter:faultwrap]
17paste.filter_factory = meteos.api.middleware.fault:FaultWrapper.factory
18
19[filter:noauth]
20paste.filter_factory = meteos.api.middleware.auth:NoAuthMiddleware.factory
21
22[filter:sizelimit]
23paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
24
25[filter:http_proxy_to_wsgi]
26paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
27
28[app:api]
29paste.app_factory = meteos.api.v1.router:APIRouter.factory
30
31[pipeline:apiversions]
32pipeline = cors faultwrap http_proxy_to_wsgi oslearningversionapp
33
34[app:oslearningversionapp]
35paste.app_factory = meteos.api.versions:VersionsRouter.factory
36
37##########
38# Engine #
39##########
40
41[filter:keystonecontext]
42paste.filter_factory = meteos.api.middleware.auth:MeteosKeystoneContext.factory
43
44[filter:authtoken]
45paste.filter_factory = keystonemiddleware.auth_token:filter_factory
46
47[filter:cors]
48paste.filter_factory = oslo_middleware.cors:filter_factory
49oslo_config_project = meteos
diff --git a/etc/meteos/logging_sample.conf b/etc/meteos/logging_sample.conf
new file mode 100644
index 0000000..2c6c3e3
--- /dev/null
+++ b/etc/meteos/logging_sample.conf
@@ -0,0 +1,73 @@
1[loggers]
2keys = root, meteos
3
4[handlers]
5keys = stderr, stdout, watchedfile, syslog, null
6
7[formatters]
8keys = default
9
10[logger_root]
11level = WARNING
12handlers = null
13
14[logger_meteos]
15level = INFO
16handlers = stderr
17qualname = meteos
18
19[logger_amqplib]
20level = WARNING
21handlers = stderr
22qualname = amqplib
23
24[logger_sqlalchemy]
25level = WARNING
26handlers = stderr
27qualname = sqlalchemy
28# "level = INFO" logs SQL queries.
29# "level = DEBUG" logs SQL queries and results.
30# "level = WARNING" logs neither. (Recommended for production systems.)
31
32[logger_boto]
33level = WARNING
34handlers = stderr
35qualname = boto
36
37[logger_suds]
38level = INFO
39handlers = stderr
40qualname = suds
41
42[logger_eventletwsgi]
43level = WARNING
44handlers = stderr
45qualname = eventlet.wsgi.server
46
47[handler_stderr]
48class = StreamHandler
49args = (sys.stderr,)
50formatter = default
51
52[handler_stdout]
53class = StreamHandler
54args = (sys.stdout,)
55formatter = default
56
57[handler_watchedfile]
58class = handlers.WatchedFileHandler
59args = ('meteos.log',)
60formatter = default
61
62[handler_syslog]
63class = handlers.SysLogHandler
64args = ('/dev/log', handlers.SysLogHandler.LOG_USER)
65formatter = default
66
67[handler_null]
68class = meteos.common.openstack.NullHandler
69formatter = default
70args = ()
71
72[formatter_default]
73format = %(message)s
diff --git a/etc/meteos/meteos.conf b/etc/meteos/meteos.conf
new file mode 100644
index 0000000..0c6f311
--- /dev/null
+++ b/etc/meteos/meteos.conf
@@ -0,0 +1,39 @@
1[DEFAULT]
2
3debug = True
4verbose = True
5log_dir = /var/log/meteos
6rpc_backend = rabbit
7
8host = 10.0.0.6
9hostname = meteos-vm
10osapi_learning_listen_port = 8989
11osapi_learning_workers = 4
12meteos-engine_workers = 4
13api_paste_config=api-paste.ini
14
15[sahara]
16auth_url = http://192.168.0.4:5000/v2.0
17
18[database]
19connection = sqlite:///etc/meteos/meteos.sqlite3
20
21[keystone_authtoken]
22
23auth_uri = http://192.168.0.4:5000/v2.0
24identity_uri=http://192.168.0.4:35357
25admin_tenant_name=services
26admin_password=9396d9e9e57545d8
27admin_user=sahara
28
29[oslo_messaging_rabbit]
30
31rabbit_host = 192.168.0.4
32rabbit_port = 5672
33rabbit_use_ssl = False
34rabbit_userid = guest
35rabbit_password = guest
36
37[engine]
38
39worker = 4
diff --git a/etc/meteos/policy.json b/etc/meteos/policy.json
new file mode 100644
index 0000000..010c2f8
--- /dev/null
+++ b/etc/meteos/policy.json
@@ -0,0 +1,13 @@
1{
2 "context_is_admin": "role:admin",
3 "admin_or_owner": "is_admin:True or project_id:%(project_id)s",
4 "default": "rule:admin_or_owner",
5
6 "admin_api": "is_admin:True",
7
8 "learning:create": "",
9 "learning:delete": "rule:default",
10 "learning:get": "rule:default",
11 "learning:get_all": "rule:default",
12 "learning:update": "rule:default"
13}
diff --git a/etc/meteos/rootwrap.conf b/etc/meteos/rootwrap.conf
new file mode 100644
index 0000000..b156f68
--- /dev/null
+++ b/etc/meteos/rootwrap.conf
@@ -0,0 +1,27 @@
1# Configuration for meteos-rootwrap
2# This file should be owned by (and only-writeable by) the root user
3
4[DEFAULT]
5# List of directories to load filter definitions from (separated by ',').
6# These directories MUST all be only writeable by root !
7filters_path=/etc/meteos/rootwrap.d,/usr/learning/meteos/rootwrap
8
9# List of directories to search executables in, in case filters do not
10# explicitely specify a full path (separated by ',')
11# If not specified, defaults to system PATH environment variable.
12# These directories MUST all be only writeable by root !
13exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/sbin,/usr/local/bin,/usr/lpp/mmfs/bin
14
15# Enable logging to syslog
16# Default value is False
17use_syslog=False
18
19# Which syslog facility to use.
20# Valid values include auth, authpriv, syslog, user0, user1...
21# Default value is 'syslog'
22syslog_log_facility=syslog
23
24# Which messages to log.
25# INFO means log all usage
26# ERROR means only log unsuccessful attempts
27syslog_log_level=ERROR
diff --git a/etc/meteos/rootwrap.d/learning.filters b/etc/meteos/rootwrap.d/learning.filters
new file mode 100644
index 0000000..90736ca
--- /dev/null
+++ b/etc/meteos/rootwrap.d/learning.filters
@@ -0,0 +1,20 @@
1# meteos-rootwrap command filters for share nodes
2# This file should be owned by (and only-writeable by) the root user
3
4[Filters]
5# meteos/utils.py : 'chown', '%s', '%s'
6chown: CommandFilter, chown, root
7# meteos/utils.py : 'cat', '%s'
8cat: CommandFilter, cat, root
9
10# meteos/share/drivers/lvm.py: 'rmdir', '%s'
11rmdir: CommandFilter, rmdir, root
12
13# meteos/share/drivers/helpers.py: 'cp', '%s', '%s'
14cp: CommandFilter, cp, root
15
16# meteos/share/drivers/helpers.py: 'service', '%s', '%s'
17service: CommandFilter, service, root
18
19# meteos/share/drivers/glusterfs.py: 'rm', '-rf', '%s'
20rm: CommandFilter, rm, root
diff --git a/etc/oslo-config-generator/meteos.conf b/etc/oslo-config-generator/meteos.conf
new file mode 100644
index 0000000..08baff1
--- /dev/null
+++ b/etc/oslo-config-generator/meteos.conf
@@ -0,0 +1,9 @@
1[DEFAULT]
2output_file = etc/meteos/meteos.conf.sample
3namespace = meteos
4namespace = oslo.messaging
5namespace = oslo.middleware.cors
6namespace = oslo.middleware.http_proxy_to_wsgi
7namespace = oslo.db
8namespace = oslo.db.concurrency
9namespace = keystonemiddleware.auth_token
diff --git a/meteos/__init__.py b/meteos/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meteos/__init__.py
diff --git a/meteos/api/__init__.py b/meteos/api/__init__.py
new file mode 100644
index 0000000..d91f63a
--- /dev/null
+++ b/meteos/api/__init__.py
@@ -0,0 +1,21 @@
1# Copyright 2010 United States Government as represented by the
2# Administrator of the National Aeronautics and Space Administration.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import paste.urlmap
18
19
20def root_app_factory(loader, global_conf, **local_conf):
21 return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
diff --git a/meteos/api/auth.py b/meteos/api/auth.py
new file mode 100644
index 0000000..0503e3f
--- /dev/null
+++ b/meteos/api/auth.py
@@ -0,0 +1,38 @@
1# Copyright (c) 2013 OpenStack, LLC.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from oslo_log import log
18
19from meteos.api.middleware import auth
20from meteos.i18n import _LW
21
22LOG = log.getLogger(__name__)
23
24
25class MeteosKeystoneContext(auth.MeteosKeystoneContext):
26 def __init__(self, application):
27 LOG.warning(_LW('meteos.api.auth:MeteosKeystoneContext is deprecated. '
28 'Please use '
29 'meteos.api.middleware.auth:MeteosKeystoneContext '
30 'instead.'))
31 super(MeteosKeystoneContext, self).__init__(application)
32
33
34def pipeline_factory(loader, global_conf, **local_conf):
35 LOG.warning(_LW('meteos.api.auth:pipeline_factory is deprecated. '
36 'Please use meteos.api.middleware.auth:pipeline_factory '
37 'instead.'))
38 auth.pipeline_factory(loader, global_conf, **local_conf)
diff --git a/meteos/api/common.py b/meteos/api/common.py
new file mode 100644
index 0000000..298faf4
--- /dev/null
+++ b/meteos/api/common.py
@@ -0,0 +1,318 @@
1# Copyright 2010 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import os
17import re
18
19from oslo_config import cfg
20from oslo_log import log
21from six.moves.urllib import parse
22import webob
23
24from meteos.api.openstack import api_version_request as api_version
25from meteos.api.openstack import versioned_method
26from meteos.i18n import _
27
28api_common_opts = [
29 cfg.IntOpt(
30 'osapi_max_limit',
31 default=1000,
32 help='The maximum number of items returned in a single response from '
33 'a collection resource.'),
34 cfg.StrOpt(
35 'osapi_learning_base_URL',
36 help='Base URL to be presented to users in links to the Learning API'),
37]
38
39CONF = cfg.CONF
40CONF.register_opts(api_common_opts)
41LOG = log.getLogger(__name__)
42
43
44# Regex that matches alphanumeric characters, periods, hypens,
45# colons and underscores:
46# ^ assert position at start of the string
47# [\w\.\-\:\_] match expression
48# $ assert position at end of the string
49VALID_KEY_NAME_REGEX = re.compile(r"^[\w\.\-\:\_]+$", re.UNICODE)
50
51
52def validate_key_names(key_names_list):
53 """Validate each item of the list to match key name regex."""
54 for key_name in key_names_list:
55 if not VALID_KEY_NAME_REGEX.match(key_name):
56 return False
57 return True
58
59
60def get_pagination_params(request):
61 """Return marker, limit tuple from request.
62
63 :param request: `wsgi.Request` possibly containing 'marker' and 'limit'
64 GET variables. 'marker' is the id of the last element
65 the client has seen, and 'limit' is the maximum number
66 of items to return. If 'limit' is not specified, 0, or
67 > max_limit, we default to max_limit. Negative values
68 for either marker or limit will cause
69 exc.HTTPBadRequest() exceptions to be raised.
70
71 """
72 params = {}
73 if 'limit' in request.GET:
74 params['limit'] = _get_limit_param(request)
75 if 'marker' in request.GET:
76 params['marker'] = _get_marker_param(request)
77 return params
78
79
80def _get_limit_param(request):
81 """Extract integer limit from request or fail."""
82 try:
83 limit = int(request.GET['limit'])
84 except ValueError:
85 msg = _('limit param must be an integer')
86 raise webob.exc.HTTPBadRequest(explanation=msg)
87 if limit < 0:
88 msg = _('limit param must be positive')
89 raise webob.exc.HTTPBadRequest(explanation=msg)
90 return limit
91
92
93def _get_marker_param(request):
94 """Extract marker ID from request or fail."""
95 return request.GET['marker']
96
97
98def limited(items, request, max_limit=CONF.osapi_max_limit):
99 """Return a slice of items according to requested offset and limit.
100
101 :param items: A sliceable entity
102 :param request: ``wsgi.Request`` possibly containing 'offset' and 'limit'
103 GET variables. 'offset' is where to start in the list,
104 and 'limit' is the maximum number of items to return. If
105 'limit' is not specified, 0, or > max_limit, we default
106 to max_limit. Negative values for either offset or limit
107 will cause exc.HTTPBadRequest() exceptions to be raised.
108 :kwarg max_limit: The maximum number of items to return from 'items'
109 """
110 try:
111 offset = int(request.GET.get('offset', 0))
112 except ValueError:
113 msg = _('offset param must be an integer')
114 raise webob.exc.HTTPBadRequest(explanation=msg)
115
116 try:
117 limit = int(request.GET.get('limit', max_limit))
118 except ValueError:
119 msg = _('limit param must be an integer')
120 raise webob.exc.HTTPBadRequest(explanation=msg)
121
122 if limit < 0:
123 msg = _('limit param must be positive')
124 raise webob.exc.HTTPBadRequest(explanation=msg)
125
126 if offset < 0:
127 msg = _('offset param must be positive')
128 raise webob.exc.HTTPBadRequest(explanation=msg)
129
130 limit = min(max_limit, limit or max_limit)
131 range_end = offset + limit
132 return items[offset:range_end]
133
134
135def limited_by_marker(items, request, max_limit=CONF.osapi_max_limit):
136 """Return a slice of items according to the requested marker and limit."""
137 params = get_pagination_params(request)
138
139 limit = params.get('limit', max_limit)
140 marker = params.get('marker')
141
142 limit = min(max_limit, limit)
143 start_index = 0
144 if marker:
145 start_index = -1
146 for i, item in enumerate(items):
147 if 'flavorid' in item:
148 if item['flavorid'] == marker:
149 start_index = i + 1
150 break
151 elif item['id'] == marker or item.get('uuid') == marker:
152 start_index = i + 1
153 break
154 if start_index < 0:
155 msg = _('marker [%s] not found') % marker
156 raise webob.exc.HTTPBadRequest(explanation=msg)
157 range_end = start_index + limit
158 return items[start_index:range_end]
159
160
161def remove_version_from_href(href):
162 """Removes the first api version from the href.
163
164 Given: 'http://www.meteos.com/v1.1/123'
165 Returns: 'http://www.meteos.com/123'
166
167 Given: 'http://www.meteos.com/v1.1'
168 Returns: 'http://www.meteos.com'
169
170 """
171 parsed_url = parse.urlsplit(href)
172 url_parts = parsed_url.path.split('/', 2)
173
174 # NOTE: this should match vX.X or vX
175 expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
176 if expression.match(url_parts[1]):
177 del url_parts[1]
178
179 new_path = '/'.join(url_parts)
180
181 if new_path == parsed_url.path:
182 msg = 'href %s does not contain version' % href
183 LOG.debug(msg)
184 raise ValueError(msg)
185
186 parsed_url = list(parsed_url)
187 parsed_url[2] = new_path
188 return parse.urlunsplit(parsed_url)
189
190
191def dict_to_query_str(params):
192 # TODO(throughnothing): we should just use urllib.urlencode instead of this
193 # But currently we don't work with urlencoded url's
194 param_str = ""
195 for key, val in params.items():
196 param_str = param_str + '='.join([str(key), str(val)]) + '&'
197
198 return param_str.rstrip('&')
199
200
201class ViewBuilder(object):
202 """Model API responses as dictionaries."""
203
204 _collection_name = None
205 _detail_version_modifiers = []
206
207 def _get_links(self, request, identifier):
208 return [{"rel": "self",
209 "href": self._get_href_link(request, identifier), },
210 {"rel": "bookmark",
211 "href": self._get_bookmark_link(request, identifier), }]
212
213 def _get_next_link(self, request, identifier):
214 """Return href string with proper limit and marker params."""
215 params = request.params.copy()
216 params["marker"] = identifier
217 prefix = self._update_link_prefix(request.application_url,
218 CONF.osapi_learning_base_URL)
219 url = os.path.join(prefix,
220 request.environ["meteos.context"].project_id,
221 self._collection_name)
222 return "%s?%s" % (url, dict_to_query_str(params))
223
224 def _get_href_link(self, request, identifier):
225 """Return an href string pointing to this object."""
226 prefix = self._update_link_prefix(request.application_url,
227 CONF.osapi_learning_base_URL)
228 return os.path.join(prefix,
229 request.environ["meteos.context"].project_id,
230 self._collection_name,
231 str(identifier))
232
233 def _get_bookmark_link(self, request, identifier):
234 """Create a URL that refers to a specific resource."""
235 base_url = remove_version_from_href(request.application_url)
236 base_url = self._update_link_prefix(base_url,
237 CONF.osapi_learning_base_URL)
238 return os.path.join(base_url,
239 request.environ["meteos.context"].project_id,
240 self._collection_name,
241 str(identifier))
242
243 def _get_collection_links(self, request, items, id_key="uuid"):
244 """Retrieve 'next' link, if applicable."""
245 links = []
246 limit = int(request.params.get("limit", 0))
247 if limit and limit == len(items):
248 last_item = items[-1]
249 if id_key in last_item:
250 last_item_id = last_item[id_key]
251 else:
252 last_item_id = last_item["id"]
253 links.append({
254 "rel": "next",
255 "href": self._get_next_link(request, last_item_id),
256 })
257 return links
258
259 def _update_link_prefix(self, orig_url, prefix):
260 if not prefix:
261 return orig_url
262 url_parts = list(parse.urlsplit(orig_url))
263 prefix_parts = list(parse.urlsplit(prefix))
264 url_parts[0:2] = prefix_parts[0:2]
265 return parse.urlunsplit(url_parts)
266
267 def update_versioned_resource_dict(self, request, resource_dict, resource):
268 """Updates the given resource dict for the given request version.
269
270 This method calls every method, that is applicable to the request
271 version, in _detail_version_modifiers.
272 """
273 for method_name in self._detail_version_modifiers:
274 method = getattr(self, method_name)
275 if request.api_version_request.matches_versioned_method(method):
276 request_context = request.environ['meteos.context']
277 method.func(self, request_context, resource_dict, resource)
278
279 @classmethod
280 def versioned_method(cls, min_ver, max_ver=None, experimental=False):
281 """Decorator for versioning API methods.
282
283 :param min_ver: string representing minimum version
284 :param max_ver: optional string representing maximum version
285 :param experimental: flag indicating an API is experimental and is
286 subject to change or removal at any time
287 """
288
289 def decorator(f):
290 obj_min_ver = api_version.APIVersionRequest(min_ver)
291 if max_ver:
292 obj_max_ver = api_version.APIVersionRequest(max_ver)
293 else:
294 obj_max_ver = api_version.APIVersionRequest()
295
296 # Add to list of versioned methods registered
297 func_name = f.__name__
298 new_func = versioned_method.VersionedMethod(
299 func_name, obj_min_ver, obj_max_ver, experimental, f)
300
301 return new_func
302
303 return decorator
304
305
306def remove_invalid_options(context, search_options, allowed_search_options):
307 """Remove search options that are not valid for non-admin API/context."""
308 if context.is_admin:
309 # Allow all options
310 return
311 # Otherwise, strip out all unknown options
312 unknown_options = [opt for opt in search_options
313 if opt not in allowed_search_options]
314 bad_options = ", ".join(unknown_options)
315 LOG.debug("Removing options '%(bad_options)s' from query",
316 {"bad_options": bad_options})
317 for opt in unknown_options:
318 del search_options[opt]
diff --git a/meteos/api/contrib/__init__.py b/meteos/api/contrib/__init__.py
new file mode 100644
index 0000000..5c12530
--- /dev/null
+++ b/meteos/api/contrib/__init__.py
@@ -0,0 +1,37 @@
1# Copyright 2011 Justin Santa Barbara
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""Contrib contains extensions that are shipped with meteos.
17
18It can't be called 'extensions' because that causes namespacing problems.
19
20"""
21
22from oslo_config import cfg
23from oslo_log import log
24
25from meteos.api import extensions
26
27CONF = cfg.CONF
28LOG = log.getLogger(__name__)
29
30
31def standard_extensions(ext_mgr):
32 extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__)
33
34
35def select_extensions(ext_mgr):
36 extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__,
37 CONF.osapi_learning_ext_list)
diff --git a/meteos/api/middleware/__init__.py b/meteos/api/middleware/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meteos/api/middleware/__init__.py
diff --git a/meteos/api/middleware/auth.py b/meteos/api/middleware/auth.py
new file mode 100644
index 0000000..3f79e80
--- /dev/null
+++ b/meteos/api/middleware/auth.py
@@ -0,0 +1,150 @@
1# Copyright 2010 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15"""
16Common Auth Middleware.
17
18"""
19import os
20
21from oslo_config import cfg
22from oslo_log import log
23from oslo_serialization import jsonutils
24import webob.dec
25import webob.exc
26
27from meteos.api.openstack import wsgi
28from meteos import context
29from meteos.i18n import _
30from meteos import wsgi as base_wsgi
31
32use_forwarded_for_opt = cfg.BoolOpt(
33 'use_forwarded_for',
34 default=False,
35 help='Treat X-Forwarded-For as the canonical remote address. '
36 'Only enable this if you have a sanitizing proxy.')
37
38CONF = cfg.CONF
39CONF.register_opt(use_forwarded_for_opt)
40LOG = log.getLogger(__name__)
41
42
43def pipeline_factory(loader, global_conf, **local_conf):
44 """A paste pipeline replica that keys off of auth_strategy."""
45 pipeline = local_conf[CONF.auth_strategy]
46 if not CONF.api_rate_limit:
47 limit_name = CONF.auth_strategy + '_nolimit'
48 pipeline = local_conf.get(limit_name, pipeline)
49 pipeline = pipeline.split()
50 filters = [loader.get_filter(n) for n in pipeline[:-1]]
51 app = loader.get_app(pipeline[-1])
52 filters.reverse()
53 for filter in filters:
54 app = filter(app)
55 return app
56
57
58class InjectContext(base_wsgi.Middleware):
59 """Add a 'meteos.context' to WSGI environ."""
60
61 def __init__(self, context, *args, **kwargs):
62 self.context = context
63 super(InjectContext, self).__init__(*args, **kwargs)
64
65 @webob.dec.wsgify(RequestClass=base_wsgi.Request)
66 def __call__(self, req):
67 req.environ['meteos.context'] = self.context
68 return self.application
69
70
71class MeteosKeystoneContext(base_wsgi.Middleware):
72 """Make a request context from keystone headers."""
73
74 @webob.dec.wsgify(RequestClass=base_wsgi.Request)
75 def __call__(self, req):
76 user_id = req.headers.get('X_USER')
77 user_id = req.headers.get('X_USER_ID', user_id)
78 if user_id is None:
79 LOG.debug("Neither X_USER_ID nor X_USER found in request")
80 return webob.exc.HTTPUnauthorized()
81 # get the roles
82 roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
83 if 'X_TENANT_ID' in req.headers:
84 # This is the new header since Keystone went to ID/Name
85 project_id = req.headers['X_TENANT_ID']
86 else:
87 # This is for legacy compatibility
88 project_id = req.headers['X_TENANT']
89
90 # Get the auth token
91 auth_token = req.headers.get('X_AUTH_TOKEN',
92 req.headers.get('X_STORAGE_TOKEN'))
93
94 # Build a context, including the auth_token...
95 remote_address = req.remote_addr
96 if CONF.use_forwarded_for:
97 remote_address = req.headers.get('X-Forwarded-For', remote_address)
98
99 service_catalog = None
100 if req.headers.get('X_SERVICE_CATALOG') is not None:
101 try:
102 catalog_header = req.headers.get('X_SERVICE_CATALOG')
103 service_catalog = jsonutils.loads(catalog_header)
104 except ValueError:
105 raise webob.exc.HTTPInternalServerError(
106 _('Invalid service catalog json.'))
107
108 ctx = context.RequestContext(user_id,
109 project_id,
110 roles=roles,
111 auth_token=auth_token,
112 remote_address=remote_address,
113 service_catalog=service_catalog)
114
115 req.environ['meteos.context'] = ctx
116 return self.application
117
118
119class NoAuthMiddleware(base_wsgi.Middleware):
120 """Return a fake token if one isn't specified."""
121
122 @webob.dec.wsgify(RequestClass=wsgi.Request)
123 def __call__(self, req):
124 if 'X-Auth-Token' not in req.headers:
125 user_id = req.headers.get('X-Auth-User', 'admin')
126 project_id = req.headers.get('X-Auth-Project-Id', 'admin')
127 os_url = os.path.join(req.url, project_id)
128 res = webob.Response()
129 # NOTE(vish): This is expecting and returning Auth(1.1), whereas
130 # keystone uses 2.0 auth. We should probably allow
131 # 2.0 auth here as well.
132 res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id)
133 res.headers['X-Server-Management-Url'] = os_url
134 res.content_type = 'text/plain'
135 res.status = '204'
136 return res
137
138 token = req.headers['X-Auth-Token']
139 user_id, _sep, project_id = token.partition(':')
140 project_id = project_id or user_id
141 remote_address = getattr(req, 'remote_address', '127.0.0.1')
142 if CONF.use_forwarded_for:
143 remote_address = req.headers.get('X-Forwarded-For', remote_address)
144 ctx = context.RequestContext(user_id,
145 project_id,
146 is_admin=True,
147 remote_address=remote_address)
148
149 req.environ['meteos.context'] = ctx
150 return self.application
diff --git a/meteos/api/middleware/fault.py b/meteos/api/middleware/fault.py
new file mode 100644
index 0000000..056cbcd
--- /dev/null
+++ b/meteos/api/middleware/fault.py
@@ -0,0 +1,74 @@
1# Copyright 2010 United States Government as represented by the
2# Administrator of the National Aeronautics and Space Administration.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from oslo_log import log
18import six
19import webob.dec
20import webob.exc
21
22from meteos.api.openstack import wsgi
23from meteos.i18n import _LE, _LI
24from meteos import utils
25from meteos import wsgi as base_wsgi
26
27LOG = log.getLogger(__name__)
28
29
30class FaultWrapper(base_wsgi.Middleware):
31 """Calls down the middleware stack, making exceptions into faults."""
32
33 _status_to_type = {}
34
35 @staticmethod
36 def status_to_type(status):
37 if not FaultWrapper._status_to_type:
38 for clazz in utils.walk_class_hierarchy(webob.exc.HTTPError):
39 FaultWrapper._status_to_type[clazz.code] = clazz
40 return FaultWrapper._status_to_type.get(
41 status, webob.exc.HTTPInternalServerError)()
42
43 def _error(self, inner, req):
44 LOG.exception(_LE("Caught error: %s"), six.text_type(inner))
45
46 safe = getattr(inner, 'safe', False)
47 headers = getattr(inner, 'headers', None)
48 status = getattr(inner, 'code', 500)
49 if status is None:
50 status = 500
51
52 msg_dict = dict(url=req.url, status=status)
53 LOG.info(_LI("%(url)s returned with HTTP %(status)d"), msg_dict)
54 outer = self.status_to_type(status)
55 if headers:
56 outer.headers = headers
57 # NOTE(johannes): We leave the explanation empty here on
58 # purpose. It could possibly have sensitive information
59 # that should not be returned back to the user. See
60 # bugs 868360 and 874472
61 # NOTE(eglynn): However, it would be over-conservative and
62 # inconsistent with the EC2 API to hide every exception,
63 # including those that are safe to expose, see bug 1021373
64 if safe:
65 outer.explanation = '%s: %s' % (inner.__class__.__name__,
66 six.text_type(inner))
67 return wsgi.Fault(outer)
68
69 @webob.dec.wsgify(RequestClass=wsgi.Request)
70 def __call__(self, req):
71 try:
72 return req.get_response(self.application)
73 except Exception as ex:
74 return self._error(ex, req)
diff --git a/meteos/api/openstack/__init__.py b/meteos/api/openstack/__init__.py
new file mode 100644
index 0000000..32b5c02
--- /dev/null
+++ b/meteos/api/openstack/__init__.py
@@ -0,0 +1,81 @@
1# Copyright (c) 2013 OpenStack, LLC.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17"""
18WSGI middleware for OpenStack API controllers.
19"""
20
21from oslo_log import log
22import routes
23
24from meteos.api.openstack import wsgi
25from meteos.i18n import _, _LW
26from meteos import wsgi as base_wsgi
27
28LOG = log.getLogger(__name__)
29
30
31class APIMapper(routes.Mapper):
32 def routematch(self, url=None, environ=None):
33 if url is "":
34 result = self._match("", environ)
35 return result[0], result[1]
36 return routes.Mapper.routematch(self, url, environ)
37
38
39class ProjectMapper(APIMapper):
40 def resource(self, member_name, collection_name, **kwargs):
41 if 'parent_resource' not in kwargs:
42 kwargs['path_prefix'] = '{project_id}/'
43 else:
44 parent_resource = kwargs['parent_resource']
45 p_collection = parent_resource['collection_name']
46 p_member = parent_resource['member_name']
47 kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection,
48 p_member)
49 routes.Mapper.resource(self,
50 member_name,
51 collection_name,
52 **kwargs)
53
54
55class APIRouter(base_wsgi.Router):
56 """Routes requests on the API to the appropriate controller and method."""
57 ExtensionManager = None # override in subclasses
58
59 @classmethod
60 def factory(cls, global_config, **local_config):
61 """Simple paste factory, :class:`meteos.wsgi.Router` doesn't have."""
62 return cls()
63
64 def __init__(self, ext_mgr=None):
65 mapper = ProjectMapper()
66 self.resources = {}
67 self._setup_routes(mapper, ext_mgr)
68 super(APIRouter, self).__init__(mapper)
69
70 def _setup_routes(self, mapper, ext_mgr):
71 raise NotImplementedError
72
73
74class FaultWrapper(base_wsgi.Middleware):
75 def __init__(self, application):
76 LOG.warning(_LW('meteos.api.openstack:FaultWrapper is deprecated. '
77 'Please use '
78 'meteos.api.middleware.fault:FaultWrapper instead.'))
79 # Avoid circular imports from here.
80 from meteos.api.middleware import fault
81 super(FaultWrapper, self).__init__(fault.FaultWrapper(application))
diff --git a/meteos/api/openstack/api_version_request.py b/meteos/api/openstack/api_version_request.py
new file mode 100644
index 0000000..f0ce3d1
--- /dev/null
+++ b/meteos/api/openstack/api_version_request.py
@@ -0,0 +1,169 @@
1# Copyright 2014 IBM Corp.
2# Copyright 2015 Clinton Knight
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import re
18
19from meteos.api.openstack import versioned_method
20from meteos import exception
21from meteos.i18n import _
22from meteos import utils
23
24# Define the minimum and maximum version of the API across all of the
25# REST API. The format of the version is:
26# X.Y where:
27#
28# - X will only be changed if a significant backwards incompatible API
29# change is made which affects the API as whole. That is, something
30# that is only very very rarely incremented.
31#
32# - Y when you make any change to the API. Note that this includes
33# semantic changes which may not affect the input or output formats or
34# even originate in the API code layer. We are not distinguishing
35# between backwards compatible and backwards incompatible changes in
36# the versioning system. It must be made clear in the documentation as
37# to what is a backwards compatible change and what is a backwards
38# incompatible one.
39
40#
41# You must update the API version history string below with a one or
42# two line description as well as update rest_api_version_history.rst
43REST_API_VERSION_HISTORY = """
44
45 REST API Version History:
46
47 * 1.0 - Initial version. Includes all V1 APIs and extensions in Mitaka.
48"""
49
50# The minimum and maximum versions of the API supported
51# The default api version request is defined to be the
52# the minimum version of the API supported.
53_MIN_API_VERSION = "1.0"
54_MAX_API_VERSION = "1.0"
55DEFAULT_API_VERSION = _MIN_API_VERSION
56
57
58# NOTE(cyeoh): min and max versions declared as functions so we can
59# mock them for unittests. Do not use the constants directly anywhere
60# else.
61def min_api_version():
62 return APIVersionRequest(_MIN_API_VERSION)
63
64
65def max_api_version():
66 return APIVersionRequest(_MAX_API_VERSION)
67
68
69class APIVersionRequest(utils.ComparableMixin):
70 """This class represents an API Version Request.
71
72 This class includes convenience methods for manipulation
73 and comparison of version numbers as needed to implement
74 API microversions.
75 """
76
77 def __init__(self, version_string=None, experimental=False):
78 """Create an API version request object."""
79 self._ver_major = None
80 self._ver_minor = None
81 self._experimental = experimental
82
83 if version_string is not None:
84 match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
85 version_string)
86 if match:
87 self._ver_major = int(match.group(1))
88 self._ver_minor = int(match.group(2))
89 else:
90 raise exception.InvalidAPIVersionString(version=version_string)
91
92 def __str__(self):
93 """Debug/Logging representation of object."""
94 return ("API Version Request Major: %(major)s, Minor: %(minor)s"
95 % {'major': self._ver_major, 'minor': self._ver_minor})
96
97 def is_null(self):
98 return self._ver_major is None and self._ver_minor is None
99
100 def _cmpkey(self):
101 """Return the value used by ComparableMixin for rich comparisons."""
102 return self._ver_major, self._ver_minor
103
104 @property
105 def experimental(self):
106 return self._experimental
107
108 @experimental.setter
109 def experimental(self, value):
110 if type(value) != bool:
111 msg = _('The experimental property must be a bool value.')
112 raise exception.InvalidParameterValue(err=msg)
113 self._experimental = value
114
115 def matches_versioned_method(self, method):
116 """Compares this version to that of a versioned method."""
117
118 if type(method) != versioned_method.VersionedMethod:
119 msg = _('An API version request must be compared '
120 'to a VersionedMethod object.')
121 raise exception.InvalidParameterValue(err=msg)
122
123 return self.matches(method.start_version,
124 method.end_version,
125 method.experimental)
126
127 def matches(self, min_version, max_version, experimental=False):
128 """Compares this version to the specified min/max range.
129
130 Returns whether the version object represents a version
131 greater than or equal to the minimum version and less than
132 or equal to the maximum version.
133
134 If min_version is null then there is no minimum limit.
135 If max_version is null then there is no maximum limit.
136 If self is null then raise ValueError.
137
138 :param min_version: Minimum acceptable version.
139 :param max_version: Maximum acceptable version.
140 :param experimental: Whether to match experimental APIs.
141 :returns: boolean
142 """
143
144 if self.is_null():
145 raise ValueError
146 # NOTE(cknight): An experimental request should still match a
147 # non-experimental API, so the experimental check isn't just
148 # looking for equality.
149 if not self.experimental and experimental:
150 return False
151 if max_version.is_null() and min_version.is_null():
152 return True
153 elif max_version.is_null():
154 return min_version <= self
155 elif min_version.is_null():
156 return self <= max_version
157 else:
158 return min_version <= self <= max_version
159
160 def get_string(self):
161 """Returns a string representation of this object.
162
163 If this method is used to create an APIVersionRequest,
164 the resulting object will be an equivalent request.
165 """
166 if self.is_null():
167 raise ValueError
168 return ("%(major)s.%(minor)s" %
169 {'major': self._ver_major, 'minor': self._ver_minor})
diff --git a/meteos/api/openstack/rest_api_version_history.rst b/meteos/api/openstack/rest_api_version_history.rst
new file mode 100644
index 0000000..ae37be6
--- /dev/null
+++ b/meteos/api/openstack/rest_api_version_history.rst
@@ -0,0 +1,14 @@
1REST API Version History
2========================
3
4This documents the changes made to the REST API with every
5microversion change. The description for each version should be a
6verbose one which has enough information to be suitable for use in
7user documentation.
8
91.0
10---
11 The 1.0 Meteos API includes all v1 core APIs existing prior to
12 the introduction of microversions. The /v1 URL is used to call
13 1.0 APIs, and microversions headers sent to this endpoint are
14 ignored.
diff --git a/meteos/api/openstack/urlmap.py b/meteos/api/openstack/urlmap.py
new file mode 100644
index 0000000..8ff1dfe
--- /dev/null
+++ b/meteos/api/openstack/urlmap.py
@@ -0,0 +1,29 @@
1# Copyright (c) 2013 OpenStack, LLC.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from oslo_log import log
18
19from meteos.api import urlmap
20from meteos.i18n import _LW
21
22LOG = log.getLogger(__name__)
23
24
25def urlmap_factory(loader, global_conf, **local_conf):
26 LOG.warning(_LW('meteos.api.openstack.urlmap:urlmap_factory '
27 'is deprecated. '
28 'Please use meteos.api.urlmap:urlmap_factory instead.'))
29 urlmap.urlmap_factory(loader, global_conf, **local_conf)
diff --git a/meteos/api/openstack/versioned_method.py b/meteos/api/openstack/versioned_method.py
new file mode 100644
index 0000000..28ab516
--- /dev/null
+++ b/meteos/api/openstack/versioned_method.py
@@ -0,0 +1,49 @@
1# Copyright 2014 IBM Corp.
2# Copyright 2015 Clinton Knight
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from meteos import utils
18
19
20class VersionedMethod(utils.ComparableMixin):
21
22 def __init__(self, name, start_version, end_version, experimental, func):
23 """Versioning information for a single method.
24
25 Minimum and maximums are inclusive.
26
27 :param name: Name of the method
28 :param start_version: Minimum acceptable version
29 :param end_version: Maximum acceptable_version
30 :param experimental: True if method is experimental
31 :param func: Method to call
32 """
33 self.name = name
34 self.start_version = start_version
35 self.end_version = end_version
36 self.experimental = experimental
37 self.func = func
38
39 def __str__(self):
40 args = {
41 'name': self.name,
42 'start': self.start_version,
43 'end': self.end_version
44 }
45 return ("Version Method %(name)s: min: %(start)s, max: %(end)s" % args)
46
47 def _cmpkey(self):
48 """Return the value used by ComparableMixin for rich comparisons."""
49 return self.start_version
diff --git a/meteos/api/openstack/wsgi.py b/meteos/api/openstack/wsgi.py
new file mode 100644
index 0000000..de1a001
--- /dev/null
+++ b/meteos/api/openstack/wsgi.py
@@ -0,0 +1,1343 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import functools
17import inspect
18import math
19import time
20
21from oslo_log import log
22from oslo_serialization import jsonutils
23from oslo_utils import strutils
24import six
25import webob
26import webob.exc
27
28from meteos.api.openstack import api_version_request as api_version
29from meteos.api.openstack import versioned_method
30from meteos.common import constants
31from meteos import exception
32from meteos.i18n import _, _LE, _LI
33from meteos import policy
34from meteos import wsgi
35
36LOG = log.getLogger(__name__)
37
38SUPPORTED_CONTENT_TYPES = (
39 'application/json',
40)
41
42_MEDIA_TYPE_MAP = {
43 'application/json': 'json',
44}
45
46# name of attribute to keep version method information
47VER_METHOD_ATTR = 'versioned_methods'
48
49# Name of header used by clients to request a specific version
50# of the REST API
51API_VERSION_REQUEST_HEADER = 'X-OpenStack-Meteos-API-Version'
52EXPERIMENTAL_API_REQUEST_HEADER = 'X-OpenStack-Meteos-API-Experimental'
53
54V1_SCRIPT_NAME = '/v1'
55
56
57class Request(webob.Request):
58 """Add some OpenStack API-specific logic to the base webob.Request."""
59
60 def __init__(self, *args, **kwargs):
61 super(Request, self).__init__(*args, **kwargs)
62 self._resource_cache = {}
63 if not hasattr(self, 'api_version_request'):
64 self.api_version_request = api_version.APIVersionRequest()
65
66 def cache_resource(self, resource_to_cache, id_attribute='id', name=None):
67 """Cache the given resource.
68
69 Allow API methods to cache objects, such as results from a DB query,
70 to be used by API extensions within the same API request.
71
72 The resource_to_cache can be a list or an individual resource,
73 but ultimately resources are cached individually using the given
74 id_attribute.
75
76 Different resources types might need to be cached during the same
77 request, they can be cached using the name parameter. For example:
78
79 Controller 1:
80 request.cache_resource(db_volumes, 'volumes')
81 request.cache_resource(db_volume_types, 'types')
82 Controller 2:
83 db_volumes = request.cached_resource('volumes')
84 db_type_1 = request.cached_resource_by_id('1', 'types')
85
86 If no name is given, a default name will be used for the resource.
87
88 An instance of this class only lives for the lifetime of a
89 single API request, so there's no need to implement full
90 cache management.
91 """
92 if not isinstance(resource_to_cache, list):
93 resource_to_cache = [resource_to_cache]
94 if not name:
95 name = self.path
96 cached_resources = self._resource_cache.setdefault(name, {})
97 for resource in resource_to_cache:
98 cached_resources[resource[id_attribute]] = resource
99
100 def cached_resource(self, name=None):
101 """Get the cached resources cached under the given resource name.
102
103 Allow an API extension to get previously stored objects within
104 the same API request.
105
106 Note that the object data will be slightly stale.
107
108 :returns: a dict of id_attribute to the resource from the cached
109 resources, an empty map if an empty collection was cached,
110 or None if nothing has been cached yet under this name
111 """
112 if not name:
113 name = self.path
114 if name not in self._resource_cache:
115 # Nothing has been cached for this key yet
116 return None
117 return self._resource_cache[name]
118
119 def cached_resource_by_id(self, resource_id, name=None):
120 """Get a resource by ID cached under the given resource name.
121
122 Allow an API extension to get a previously stored object
123 within the same API request. This is basically a convenience method
124 to lookup by ID on the dictionary of all cached resources.
125
126 Note that the object data will be slightly stale.
127
128 :returns: the cached resource or None if the item is not in the cache
129 """
130 resources = self.cached_resource(name)
131 if not resources:
132 # Nothing has been cached yet for this key yet
133 return None
134 return resources.get(resource_id)
135
136 def cache_db_items(self, key, items, item_key='id'):
137 """Cache db items.
138
139 Allow API methods to store objects from a DB query to be
140 used by API extensions within the same API request.
141 An instance of this class only lives for the lifetime of a
142 single API request, so there's no need to implement full
143 cache management.
144 """
145 self.cache_resource(items, item_key, key)
146
147 def get_db_items(self, key):
148 """Get db item by key.
149
150 Allow an API extension to get previously stored objects within
151 the same API request.
152 Note that the object data will be slightly stale.
153 """
154 return self.cached_resource(key)
155
156 def get_db_item(self, key, item_key):
157 """Get db item by key and item key.
158
159 Allow an API extension to get a previously stored object
160 within the same API request.
161 Note that the object data will be slightly stale.
162 """
163 return self.get_db_items(key).get(item_key)
164
165 def cache_db_learning_types(self, learning_types):
166 self.cache_db_items('learning_types', learning_types, 'id')
167
168 def cache_db_learning_type(self, learning_type):
169 self.cache_db_items('learning_types', [learning_type], 'id')
170
171 def get_db_learning_types(self):
172 return self.get_db_items('learning_types')
173
174 def get_db_learning_type(self, learning_type_id):
175 return self.get_db_item('learning_types', learning_type_id)
176
177 def best_match_content_type(self):
178 """Determine the requested response content-type."""
179 if 'meteos.best_content_type' not in self.environ:
180 # Calculate the best MIME type
181 content_type = None
182
183 # Check URL path suffix
184 parts = self.path.rsplit('.', 1)
185 if len(parts) > 1:
186 possible_type = 'application/' + parts[1]
187 if possible_type in SUPPORTED_CONTENT_TYPES:
188 content_type = possible_type
189
190 if not content_type:
191 content_type = self.accept.best_match(SUPPORTED_CONTENT_TYPES)
192
193 self.environ['meteos.best_content_type'] = (content_type or
194 'application/json')
195
196 return self.environ['meteos.best_content_type']
197
198 def get_content_type(self):
199 """Determine content type of the request body.
200
201 Does not do any body introspection, only checks header.
202 """
203 if "Content-Type" not in self.headers:
204 return None
205
206 allowed_types = SUPPORTED_CONTENT_TYPES
207 content_type = self.content_type
208
209 if content_type not in allowed_types:
210 raise exception.InvalidContentType(content_type=content_type)
211
212 return content_type
213
214 def set_api_version_request(self):
215 """Set API version request based on the request header information.
216
217 Microversions starts with /v2, so if a client sends a /v1 URL, then
218 ignore the headers and request 1.0 APIs.
219 """
220
221 if not self.script_name:
222 self.api_version_request = api_version.APIVersionRequest()
223 elif self.script_name == V1_SCRIPT_NAME:
224 self.api_version_request = api_version.APIVersionRequest('1.0')
225 else:
226 if API_VERSION_REQUEST_HEADER in self.headers:
227 hdr_string = self.headers[API_VERSION_REQUEST_HEADER]
228 self.api_version_request = api_version.APIVersionRequest(
229 hdr_string)
230
231 # Check that the version requested is within the global
232 # minimum/maximum of supported API versions
233 if not self.api_version_request.matches(
234 api_version.min_api_version(),
235 api_version.max_api_version()):
236 raise exception.InvalidGlobalAPIVersion(
237 req_ver=self.api_version_request.get_string(),
238 min_ver=api_version.min_api_version().get_string(),
239 max_ver=api_version.max_api_version().get_string())
240
241 else:
242 self.api_version_request = api_version.APIVersionRequest(
243 api_version.DEFAULT_API_VERSION)
244
245 # Check if experimental API was requested
246 if EXPERIMENTAL_API_REQUEST_HEADER in self.headers:
247 self.api_version_request.experimental = strutils.bool_from_string(
248 self.headers[EXPERIMENTAL_API_REQUEST_HEADER])
249
250
251class ActionDispatcher(object):
252 """Maps method name to local methods through action name."""
253
254 def dispatch(self, *args, **kwargs):
255 """Find and call local method."""
256 action = kwargs.pop('action', 'default')
257 action_method = getattr(self, six.text_type(action), self.default)
258 return action_method(*args, **kwargs)
259
260 def default(self, data):
261 raise NotImplementedError()
262
263
264class TextDeserializer(ActionDispatcher):
265 """Default request body deserialization."""
266
267 def deserialize(self, datastring, action='default'):
268 return self.dispatch(datastring, action=action)
269
270 def default(self, datastring):
271 return {}
272
273
274class JSONDeserializer(TextDeserializer):
275
276 def _from_json(self, datastring):
277 try:
278 return jsonutils.loads(datastring)
279 except ValueError:
280 msg = _("cannot understand JSON")
281 raise exception.MalformedRequestBody(reason=msg)
282
283 def default(self, datastring):
284 return {'body': self._from_json(datastring)}
285
286
287class DictSerializer(ActionDispatcher):
288 """Default request body serialization."""
289
290 def serialize(self, data, action='default'):
291 return self.dispatch(data, action=action)
292
293 def default(self, data):
294 return ""
295
296
297class JSONDictSerializer(DictSerializer):
298 """Default JSON request body serialization."""
299
300 def default(self, data):
301 return six.b(jsonutils.dumps(data))
302
303
304def serializers(**serializers):
305 """Attaches serializers to a method.
306
307 This decorator associates a dictionary of serializers with a
308 method. Note that the function attributes are directly
309 manipulated; the method is not wrapped.
310 """
311
312 def decorator(func):
313 if not hasattr(func, 'wsgi_serializers'):
314 func.wsgi_serializers = {}
315 func.wsgi_serializers.update(serializers)
316 return func
317 return decorator
318
319
320def deserializers(**deserializers):
321 """Attaches deserializers to a method.
322
323 This decorator associates a dictionary of deserializers with a
324 method. Note that the function attributes are directly
325 manipulated; the method is not wrapped.
326 """
327
328 def decorator(func):
329 if not hasattr(func, 'wsgi_deserializers'):
330 func.wsgi_deserializers = {}
331 func.wsgi_deserializers.update(deserializers)
332 return func
333 return decorator
334
335
336def response(code):
337 """Attaches response code to a method.
338
339 This decorator associates a response code with a method. Note
340 that the function attributes are directly manipulated; the method
341 is not wrapped.
342 """
343
344 def decorator(func):
345 func.wsgi_code = code
346 return func
347 return decorator
348
349
350class ResponseObject(object):
351 """Bundles a response object with appropriate serializers.
352
353 Object that app methods may return in order to bind alternate
354 serializers with a response object to be serialized. Its use is
355 optional.
356 """
357
358 def __init__(self, obj, code=None, headers=None, **serializers):
359 """Binds serializers with an object.
360
361 Takes keyword arguments akin to the @serializer() decorator
362 for specifying serializers. Serializers specified will be
363 given preference over default serializers or method-specific
364 serializers on return.
365 """
366
367 self.obj = obj
368 self.serializers = serializers
369 self._default_code = 200
370 self._code = code
371 self._headers = headers or {}
372 self.serializer = None
373 self.media_type = None
374
375 def __getitem__(self, key):
376 """Retrieves a header with the given name."""
377
378 return self._headers[key.lower()]
379
380 def __setitem__(self, key, value):
381 """Sets a header with the given name to the given value."""
382
383 self._headers[key.lower()] = value
384
385 def __delitem__(self, key):
386 """Deletes the header with the given name."""
387
388 del self._headers[key.lower()]
389
390 def _bind_method_serializers(self, meth_serializers):
391 """Binds method serializers with the response object.
392
393 Binds the method serializers with the response object.
394 Serializers specified to the constructor will take precedence
395 over serializers specified to this method.
396
397 :param meth_serializers: A dictionary with keys mapping to
398 response types and values containing
399 serializer objects.
400 """
401
402 # We can't use update because that would be the wrong
403 # precedence
404 for mtype, serializer in meth_serializers.items():
405 self.serializers.setdefault(mtype, serializer)
406
407 def get_serializer(self, content_type, default_serializers=None):
408 """Returns the serializer for the wrapped object.
409
410 Returns the serializer for the wrapped object subject to the
411 indicated content type. If no serializer matching the content
412 type is attached, an appropriate serializer drawn from the
413 default serializers will be used. If no appropriate
414 serializer is available, raises InvalidContentType.
415 """
416
417 default_serializers = default_serializers or {}
418
419 try:
420 mtype = _MEDIA_TYPE_MAP.get(content_type, content_type)
421 if mtype in self.serializers:
422 return mtype, self.serializers[mtype]
423 else:
424 return mtype, default_serializers[mtype]
425 except (KeyError, TypeError):
426 raise exception.InvalidContentType(content_type=content_type)
427
428 def preserialize(self, content_type, default_serializers=None):
429 """Prepares the serializer that will be used to serialize.
430
431 Determines the serializer that will be used and prepares an
432 instance of it for later call. This allows the serializer to
433 be accessed by extensions for, e.g., template extension.
434 """
435
436 mtype, serializer = self.get_serializer(content_type,
437 default_serializers)
438 self.media_type = mtype
439 self.serializer = serializer()
440
441 def attach(self, **kwargs):
442 """Attach slave templates to serializers."""
443
444 if self.media_type in kwargs:
445 self.serializer.attach(kwargs[self.media_type])
446
447 def serialize(self, request, content_type, default_serializers=None):
448 """Serializes the wrapped object.
449
450 Utility method for serializing the wrapped object. Returns a
451 webob.Response object.
452 """
453
454 if self.serializer:
455 serializer = self.serializer
456 else:
457 _mtype, _serializer = self.get_serializer(content_type,
458 default_serializers)
459 serializer = _serializer()
460
461 response = webob.Response()
462 response.status_int = self.code
463 for hdr, value in self._headers.items():
464 response.headers[hdr] = six.text_type(value)
465 response.headers['Content-Type'] = six.text_type(content_type)
466 if self.obj is not None:
467 response.body = serializer.serialize(self.obj)
468
469 return response
470
471 @property
472 def code(self):
473 """Retrieve the response status."""
474
475 return self._code or self._default_code
476
477 @property
478 def headers(self):
479 """Retrieve the headers."""
480
481 return self._headers.copy()
482
483
484def action_peek_json(body):
485 """Determine action to invoke."""
486
487 try:
488 decoded = jsonutils.loads(body)
489 except ValueError:
490 msg = _("cannot understand JSON")
491 raise exception.MalformedRequestBody(reason=msg)
492
493 # Make sure there's exactly one key...
494 if len(decoded) != 1:
495 msg = _("too many body keys")
496 raise exception.MalformedRequestBody(reason=msg)
497
498 # Return the action and the decoded body...
499 return list(decoded.keys())[0]
500
501
502class ResourceExceptionHandler(object):
503 """Context manager to handle Resource exceptions.
504
505 Used when processing exceptions generated by API implementation
506 methods (or their extensions). Converts most exceptions to Fault
507 exceptions, with the appropriate logging.
508 """
509
510 def __enter__(self):
511 return None
512
513 def __exit__(self, ex_type, ex_value, ex_traceback):
514 if not ex_value:
515 return True
516
517 if isinstance(ex_value, exception.NotAuthorized):
518 msg = six.text_type(ex_value)
519 raise Fault(webob.exc.HTTPForbidden(explanation=msg))
520 elif isinstance(ex_value, exception.VersionNotFoundForAPIMethod):
521 raise
522 elif isinstance(ex_value, exception.Invalid):
523 raise Fault(exception.ConvertedException(
524 code=ex_value.code, explanation=six.text_type(ex_value)))
525 elif isinstance(ex_value, TypeError):
526 exc_info = (ex_type, ex_value, ex_traceback)
527 LOG.error(_LE('Exception handling resource: %s'),
528 ex_value, exc_info=exc_info)
529 raise Fault(webob.exc.HTTPBadRequest())
530 elif isinstance(ex_value, Fault):
531 LOG.info(_LI("Fault thrown: %s"), six.text_type(ex_value))
532 raise ex_value
533 elif isinstance(ex_value, webob.exc.HTTPException):
534 LOG.info(_LI("HTTP exception thrown: %s"), six.text_type(ex_value))
535 raise Fault(ex_value)
536
537 # We didn't handle the exception
538 return False
539
540
541class Resource(wsgi.Application):
542 """WSGI app that handles (de)serialization and controller dispatch.
543
544 WSGI app that reads routing information supplied by RoutesMiddleware
545 and calls the requested action method upon its controller. All
546 controller action methods must accept a 'req' argument, which is the
547 incoming wsgi.Request. If the operation is a PUT or POST, the controller
548 method must also accept a 'body' argument (the deserialized request body).
549 They may raise a webob.exc exception or return a dict, which will be
550 serialized by requested content type.
551
552 Exceptions derived from webob.exc.HTTPException will be automatically
553 wrapped in Fault() to provide API friendly error responses.
554 """
555 support_api_request_version = True
556
557 def __init__(self, controller, action_peek=None, **deserializers):
558 """init method of Resource.
559
560 :param controller: object that implement methods created by routes lib
561 :param action_peek: dictionary of routines for peeking into an action
562 request body to determine the desired action
563 """
564
565 self.controller = controller
566
567 default_deserializers = dict(json=JSONDeserializer)
568 default_deserializers.update(deserializers)
569
570 self.default_deserializers = default_deserializers
571 self.default_serializers = dict(json=JSONDictSerializer)
572
573 self.action_peek = dict(json=action_peek_json)
574 self.action_peek.update(action_peek or {})
575
576 # Copy over the actions dictionary
577 self.wsgi_actions = {}
578 if controller:
579 self.register_actions(controller)
580
581 # Save a mapping of extensions
582 self.wsgi_extensions = {}
583 self.wsgi_action_extensions = {}
584
585 def register_actions(self, controller):
586 """Registers controller actions with this resource."""
587
588 actions = getattr(controller, 'wsgi_actions', {})
589 for key, method_name in actions.items():
590 self.wsgi_actions[key] = getattr(controller, method_name)
591
592 def register_extensions(self, controller):
593 """Registers controller extensions with this resource."""
594
595 extensions = getattr(controller, 'wsgi_extensions', [])
596 for method_name, action_name in extensions:
597 # Look up the extending method
598 extension = getattr(controller, method_name)
599
600 if action_name:
601 # Extending an action...
602 if action_name not in self.wsgi_action_extensions:
603 self.wsgi_action_extensions[action_name] = []
604 self.wsgi_action_extensions[action_name].append(extension)
605 else:
606 # Extending a regular method
607 if method_name not in self.wsgi_extensions:
608 self.wsgi_extensions[method_name] = []
609 self.wsgi_extensions[method_name].append(extension)
610
611 def get_action_args(self, request_environment):
612 """Parse dictionary created by routes library."""
613
614 # NOTE(Vek): Check for get_action_args() override in the
615 # controller
616 if hasattr(self.controller, 'get_action_args'):
617 return self.controller.get_action_args(request_environment)
618
619 try:
620 args = request_environment['wsgiorg.routing_args'][1].copy()
621 except (KeyError, IndexError, AttributeError):
622 return {}
623
624 try:
625 del args['controller']
626 except KeyError:
627 pass
628
629 try:
630 del args['format']
631 except KeyError:
632 pass
633
634 return args
635
636 def get_body(self, request):
637 try:
638 content_type = request.get_content_type()
639 except exception.InvalidContentType:
640 LOG.debug("Unrecognized Content-Type provided in request")
641 return None, ''
642
643 if not content_type:
644 LOG.debug("No Content-Type provided in request")
645 return None, ''
646
647 if len(request.body) <= 0:
648 LOG.debug("Empty body provided in request")
649 return None, ''
650
651 return content_type, request.body
652
653 def deserialize(self, meth, content_type, body):
654 meth_deserializers = getattr(meth, 'wsgi_deserializers', {})
655 try:
656 mtype = _MEDIA_TYPE_MAP.get(content_type, content_type)
657 if mtype in meth_deserializers:
658 deserializer = meth_deserializers[mtype]
659 else:
660 deserializer = self.default_deserializers[mtype]
661 except (KeyError, TypeError):
662 raise exception.InvalidContentType(content_type=content_type)
663
664 return deserializer().deserialize(body)
665
666 def pre_process_extensions(self, extensions, request, action_args):
667 # List of callables for post-processing extensions
668 post = []
669
670 for ext in extensions:
671 if inspect.isgeneratorfunction(ext):
672 response = None
673
674 # If it's a generator function, the part before the
675 # yield is the preprocessing stage
676 try:
677 with ResourceExceptionHandler():
678 gen = ext(req=request, **action_args)
679 response = next(gen)
680 except Fault as ex:
681 response = ex
682
683 # We had a response...
684 if response:
685 return response, []
686
687 # No response, queue up generator for post-processing
688 post.append(gen)
689 else:
690 # Regular functions only perform post-processing
691 post.append(ext)
692
693 # Run post-processing in the reverse order
694 return None, reversed(post)
695
696 def post_process_extensions(self, extensions, resp_obj, request,
697 action_args):
698 for ext in extensions:
699 response = None
700 if inspect.isgenerator(ext):
701 # If it's a generator, run the second half of
702 # processing
703 try:
704 with ResourceExceptionHandler():
705 response = ext.send(resp_obj)
706 except StopIteration:
707 # Normal exit of generator
708 continue
709 except Fault as ex:
710 response = ex
711 else:
712 # Regular functions get post-processing...
713 try:
714 with ResourceExceptionHandler():
715 response = ext(req=request, resp_obj=resp_obj,
716 **action_args)
717 except exception.VersionNotFoundForAPIMethod:
718 # If an attached extension (@wsgi.extends) for the
719 # method has no version match its not an error. We
720 # just don't run the extends code
721 continue
722 except Fault as ex:
723 response = ex
724
725 # We had a response...
726 if response:
727 return response
728
729 return None
730
731 @webob.dec.wsgify(RequestClass=Request)
732 def __call__(self, request):
733 """WSGI method that controls (de)serialization and method dispatch."""
734
735 LOG.info(_LI("%(method)s %(url)s") % {"method": request.method,
736 "url": request.url})
737 if self.support_api_request_version:
738 # Set the version of the API requested based on the header
739 try:
740 request.set_api_version_request()
741 except exception.InvalidAPIVersionString as e:
742 return Fault(webob.exc.HTTPBadRequest(
743 explanation=six.text_type(e)))
744 except exception.InvalidGlobalAPIVersion as e:
745 return Fault(webob.exc.HTTPNotAcceptable(
746 explanation=six.text_type(e)))
747
748 # Identify the action, its arguments, and the requested
749 # content type
750 action_args = self.get_action_args(request.environ)
751 action = action_args.pop('action', None)
752 content_type, body = self.get_body(request)
753 accept = request.best_match_content_type()
754
755 # NOTE(Vek): Splitting the function up this way allows for
756 # auditing by external tools that wrap the existing
757 # function. If we try to audit __call__(), we can
758 # run into troubles due to the @webob.dec.wsgify()
759 # decorator.
760 return self._process_stack(request, action, action_args,
761 content_type, body, accept)
762
763 def _process_stack(self, request, action, action_args,
764 content_type, body, accept):
765 """Implement the processing stack."""
766
767 # Get the implementing method
768 try:
769 meth, extensions = self.get_method(request, action,
770 content_type, body)
771 except (AttributeError, TypeError):
772 return Fault(webob.exc.HTTPNotFound())
773 except KeyError as ex:
774 msg = _("There is no such action: %s") % ex.args[0]
775 return Fault(webob.exc.HTTPBadRequest(explanation=msg))
776 except exception.MalformedRequestBody:
777 msg = _("Malformed request body")
778 return Fault(webob.exc.HTTPBadRequest(explanation=msg))
779
780 if body:
781 msg = ("Action: '%(action)s', calling method: %(meth)s, body: "
782 "%(body)s") % {'action': action,
783 'body': six.text_type(body),
784 'meth': six.text_type(meth)}
785 LOG.debug(strutils.mask_password(msg))
786 else:
787 LOG.debug("Calling method '%(meth)s'",
788 {'meth': six.text_type(meth)})
789
790 # Now, deserialize the request body...
791 try:
792 if content_type:
793 contents = self.deserialize(meth, content_type, body)
794 else:
795 contents = {}
796 except exception.InvalidContentType:
797 msg = _("Unsupported Content-Type")
798 return Fault(webob.exc.HTTPBadRequest(explanation=msg))
799 except exception.MalformedRequestBody:
800 msg = _("Malformed request body")
801 return Fault(webob.exc.HTTPBadRequest(explanation=msg))
802
803 # Update the action args
804 action_args.update(contents)
805
806 project_id = action_args.pop("project_id", None)
807 context = request.environ.get('meteos.context')
808 if (context and project_id and (project_id != context.project_id)):
809 msg = _("Malformed request url")
810 return Fault(webob.exc.HTTPBadRequest(explanation=msg))
811
812 # Run pre-processing extensions
813 response, post = self.pre_process_extensions(extensions,
814 request, action_args)
815
816 if not response:
817 try:
818 with ResourceExceptionHandler():
819 action_result = self.dispatch(meth, request, action_args)
820 except Fault as ex:
821 response = ex
822
823 if not response:
824 # No exceptions; convert action_result into a
825 # ResponseObject
826 resp_obj = None
827 if type(action_result) is dict or action_result is None:
828 resp_obj = ResponseObject(action_result)
829 elif isinstance(action_result, ResponseObject):
830 resp_obj = action_result
831 else:
832 response = action_result
833
834 # Run post-processing extensions
835 if resp_obj:
836 _set_request_id_header(request, resp_obj)
837 # Do a preserialize to set up the response object
838 serializers = getattr(meth, 'wsgi_serializers', {})
839 resp_obj._bind_method_serializers(serializers)
840 if hasattr(meth, 'wsgi_code'):
841 resp_obj._default_code = meth.wsgi_code
842 resp_obj.preserialize(accept, self.default_serializers)
843
844 # Process post-processing extensions
845 response = self.post_process_extensions(post, resp_obj,
846 request, action_args)
847
848 if resp_obj and not response:
849 response = resp_obj.serialize(request, accept,
850 self.default_serializers)
851
852 try:
853 msg_dict = dict(url=request.url, status=response.status_int)
854 msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
855 except AttributeError as e:
856 msg_dict = dict(url=request.url, e=e)
857 msg = _("%(url)s returned a fault: %(e)s") % msg_dict
858
859 LOG.info(msg)
860
861 if hasattr(response, 'headers'):
862 for hdr, val in response.headers.items():
863 # Headers must be utf-8 strings
864 response.headers[hdr] = six.text_type(val)
865
866 if not request.api_version_request.is_null():
867 response.headers[API_VERSION_REQUEST_HEADER] = (
868 request.api_version_request.get_string())
869 if request.api_version_request.experimental:
870 response.headers[EXPERIMENTAL_API_REQUEST_HEADER] = (
871 request.api_version_request.experimental)
872 response.headers['Vary'] = API_VERSION_REQUEST_HEADER
873
874 return response
875
876 def get_method(self, request, action, content_type, body):
877 """Look up the action-specific method and its extensions."""
878
879 # Look up the method
880 try:
881 if not self.controller:
882 meth = getattr(self, action)
883 else:
884 meth = getattr(self.controller, action)
885 except AttributeError:
886 if (not self.wsgi_actions or
887 action not in ['action', 'create', 'delete']):
888 # Propagate the error
889 raise
890 else:
891 return meth, self.wsgi_extensions.get(action, [])
892
893 if action == 'action':
894 # OK, it's an action; figure out which action...
895 mtype = _MEDIA_TYPE_MAP.get(content_type)
896 action_name = self.action_peek[mtype](body)
897 LOG.debug("Action body: %s" % body)
898 else:
899 action_name = action
900
901 # Look up the action method
902 return (self.wsgi_actions[action_name],
903 self.wsgi_action_extensions.get(action_name, []))
904
905 def dispatch(self, method, request, action_args):
906 """Dispatch a call to the action-specific method."""
907
908 try:
909 return method(req=request, **action_args)
910 except exception.VersionNotFoundForAPIMethod:
911 # We deliberately don't return any message information
912 # about the exception to the user so it looks as if
913 # the method is simply not implemented.
914 return Fault(webob.exc.HTTPNotFound())
915
916
917def action(name):
918 """Mark a function as an action.
919
920 The given name will be taken as the action key in the body.
921
922 This is also overloaded to allow extensions to provide
923 non-extending definitions of create and delete operations.
924 """
925
926 def decorator(func):
927 func.wsgi_action = name
928 return func
929 return decorator
930
931
932def extends(*args, **kwargs):
933 """Indicate a function extends an operation.
934
935 Can be used as either::
936
937 @extends
938 def index(...):
939 pass
940
941 or as::
942
943 @extends(action='resize')
944 def _action_resize(...):
945 pass
946 """
947
948 def decorator(func):
949 # Store enough information to find what we're extending
950 func.wsgi_extends = (func.__name__, kwargs.get('action'))
951 return func
952
953 # If we have positional arguments, call the decorator
954 if args:
955 return decorator(*args)
956
957 # OK, return the decorator instead
958 return decorator
959
960
961class ControllerMetaclass(type):
962 """Controller metaclass.
963
964 This metaclass automates the task of assembling a dictionary
965 mapping action keys to method names.
966 """
967
968 def __new__(mcs, name, bases, cls_dict):
969 """Adds the wsgi_actions dictionary to the class."""
970
971 # Find all actions
972 actions = {}
973 extensions = []
974 versioned_methods = None
975 # start with wsgi actions from base classes
976 for base in bases:
977 actions.update(getattr(base, 'wsgi_actions', {}))
978
979 if base.__name__ == "Controller":
980 # NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
981 # between API controller class creations. This allows us
982 # to use a class decorator on the API methods that doesn't
983 # require naming explicitly what method is being versioned as
984 # it can be implicit based on the method decorated. It is a bit
985 # ugly.
986 if VER_METHOD_ATTR in base.__dict__:
987 versioned_methods = getattr(base, VER_METHOD_ATTR)
988 delattr(base, VER_METHOD_ATTR)
989
990 for key, value in cls_dict.items():
991 if not callable(value):
992 continue
993 if getattr(value, 'wsgi_action', None):
994 actions[value.wsgi_action] = key
995 elif getattr(value, 'wsgi_extends', None):
996 extensions.append(value.wsgi_extends)
997
998 # Add the actions and extensions to the class dict
999 cls_dict['wsgi_actions'] = actions
1000 cls_dict['wsgi_extensions'] = extensions
1001 if versioned_methods:
1002 cls_dict[VER_METHOD_ATTR] = versioned_methods
1003
1004 return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
1005 cls_dict)
1006
1007
1008@six.add_metaclass(ControllerMetaclass)
1009class Controller(object):
1010 """Default controller."""
1011
1012 _view_builder_class = None
1013
1014 def __init__(self, view_builder=None):
1015 """Initialize controller with a view builder instance."""
1016 if view_builder:
1017 self._view_builder = view_builder
1018 elif self._view_builder_class:
1019 self._view_builder = self._view_builder_class()
1020 else:
1021 self._view_builder = None
1022
1023 def __getattribute__(self, key):
1024
1025 def version_select(*args, **kwargs):
1026 """Select and call the matching version of the specified method.
1027
1028 Look for the method which matches the name supplied and version
1029 constraints and calls it with the supplied arguments.
1030
1031 :returns: Returns the result of the method called
1032 :raises: VersionNotFoundForAPIMethod if there is no method which
1033 matches the name and version constraints
1034 """
1035
1036 # The first arg to all versioned methods is always the request
1037 # object. The version for the request is attached to the
1038 # request object
1039 if len(args) == 0:
1040 version_request = kwargs['req'].api_version_request
1041 else:
1042 version_request = args[0].api_version_request
1043
1044 func_list = self.versioned_methods[key]
1045 for func in func_list:
1046 if version_request.matches_versioned_method(func):
1047 # Update the version_select wrapper function so
1048 # other decorator attributes like wsgi.response
1049 # are still respected.
1050 functools.update_wrapper(version_select, func.func)
1051 return func.func(self, *args, **kwargs)
1052
1053 # No version match
1054 raise exception.VersionNotFoundForAPIMethod(
1055 version=version_request)
1056
1057 try:
1058 version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
1059 except AttributeError:
1060 # No versioning on this class
1061 return object.__getattribute__(self, key)
1062
1063 if (version_meth_dict and
1064 key in object.__getattribute__(self, VER_METHOD_ATTR)):
1065 return version_select
1066
1067 return object.__getattribute__(self, key)
1068
1069 # NOTE(cyeoh): This decorator MUST appear first (the outermost
1070 # decorator) on an API method for it to work correctly
1071 @classmethod
1072 def api_version(cls, min_ver, max_ver=None, experimental=False):
1073 """Decorator for versioning API methods.
1074
1075 Add the decorator to any method which takes a request object
1076 as the first parameter and belongs to a class which inherits from
1077 wsgi.Controller.
1078
1079 :param min_ver: string representing minimum version
1080 :param max_ver: optional string representing maximum version
1081 :param experimental: flag indicating an API is experimental and is
1082 subject to change or removal at any time
1083 """
1084
1085 def decorator(f):
1086 obj_min_ver = api_version.APIVersionRequest(min_ver)
1087 if max_ver:
1088 obj_max_ver = api_version.APIVersionRequest(max_ver)
1089 else:
1090 obj_max_ver = api_version.APIVersionRequest()
1091
1092 # Add to list of versioned methods registered
1093 func_name = f.__name__
1094 new_func = versioned_method.VersionedMethod(
1095 func_name, obj_min_ver, obj_max_ver, experimental, f)
1096
1097 func_dict = getattr(cls, VER_METHOD_ATTR, {})
1098 if not func_dict:
1099 setattr(cls, VER_METHOD_ATTR, func_dict)
1100
1101 func_list = func_dict.get(func_name, [])
1102 if not func_list:
1103 func_dict[func_name] = func_list
1104 func_list.append(new_func)
1105 # Ensure the list is sorted by minimum version (reversed)
1106 # so later when we work through the list in order we find
1107 # the method which has the latest version which supports
1108 # the version requested.
1109 # TODO(cyeoh): Add check to ensure that there are no overlapping
1110 # ranges of valid versions as that is ambiguous
1111 func_list.sort(reverse=True)
1112
1113 return f
1114
1115 return decorator
1116
1117 @staticmethod
1118 def authorize(arg):
1119 """Decorator for checking the policy on API methods.
1120
1121 Add this decorator to any API method which takes a request object
1122 as the first parameter and belongs to a class which inherits from
1123 wsgi.Controller. The class must also have a class member called
1124 'resource_name' which specifies the resource for the policy check.
1125
1126 Can be used in any of the following forms
1127 @authorize
1128 @authorize('my_action_name')
1129
1130 :param arg: Can either be the function being decorated or a str
1131 containing the 'action' for the policy check. If no action name is
1132 provided, the function name is assumed to be the action name.
1133 """
1134 action_name = None
1135
1136 def decorator(f):
1137 @functools.wraps(f)
1138 def wrapper(self, req, *args, **kwargs):
1139 action = action_name or f.__name__
1140 context = req.environ['meteos.context']
1141 try:
1142 policy.check_policy(context, self.resource_name, action)
1143 except exception.PolicyNotAuthorized:
1144 raise webob.exc.HTTPForbidden()
1145 return f(self, req, *args, **kwargs)
1146 return wrapper
1147
1148 if callable(arg):
1149 return decorator(arg)
1150 else:
1151 action_name = arg
1152 return decorator
1153
1154 @staticmethod
1155 def is_valid_body(body, entity_name):
1156 if not (body and entity_name in body):
1157 return False
1158
1159 def is_dict(d):
1160 try:
1161 d.get(None)
1162 return True
1163 except AttributeError:
1164 return False
1165
1166 if not is_dict(body[entity_name]):
1167 return False
1168
1169 return True
1170
1171
1172class AdminActionsMixin(object):
1173 """Mixin class for API controllers with admin actions."""
1174
1175 body_attributes = {
1176 'status': 'reset_status',
1177 'replica_state': 'reset_replica_state',
1178 'task_state': 'reset_task_state',
1179 }
1180
1181 valid_statuses = {
1182 'status': set([
1183 constants.STATUS_CREATING,
1184 constants.STATUS_AVAILABLE,
1185 constants.STATUS_DELETING,
1186 constants.STATUS_ERROR,
1187 constants.STATUS_ERROR_DELETING,
1188 ]),
1189 }
1190
1191 def _update(self, *args, **kwargs):
1192 raise NotImplementedError()
1193
1194 def _get(self, *args, **kwargs):
1195 raise NotImplementedError()
1196
1197 def _delete(self, *args, **kwargs):
1198 raise NotImplementedError()
1199
1200 def validate_update(self, body, status_attr='status'):
1201 update = {}
1202 try:
1203 update[status_attr] = body[status_attr]
1204 except (TypeError, KeyError):
1205 msg = _("Must specify '%s'") % status_attr
1206 raise webob.exc.HTTPBadRequest(explanation=msg)
1207 if update[status_attr] not in self.valid_statuses[status_attr]:
1208 expl = (_("Invalid state. Valid states: %s.") %
1209 ", ".join(six.text_type(i) for i in
1210 self.valid_statuses[status_attr]))
1211 raise webob.exc.HTTPBadRequest(explanation=expl)
1212 return update
1213
1214 @Controller.authorize('reset_status')
1215 def _reset_status(self, req, id, body, status_attr='status'):
1216 """Reset the status_attr specified on the resource."""
1217 context = req.environ['meteos.context']
1218 body_attr = self.body_attributes[status_attr]
1219 update = self.validate_update(
1220 body.get(body_attr, body.get('-'.join(('os', body_attr)))),
1221 status_attr=status_attr)
1222 msg = "Updating %(resource)s '%(id)s' with '%(update)r'"
1223 LOG.debug(msg, {'resource': self.resource_name, 'id': id,
1224 'update': update})
1225 try:
1226 self._update(context, id, update)
1227 except exception.NotFound as e:
1228 raise webob.exc.HTTPNotFound(six.text_type(e))
1229 return webob.Response(status_int=202)
1230
1231 @Controller.authorize('force_delete')
1232 def _force_delete(self, req, id, body):
1233 """Delete a resource, bypassing the check for status."""
1234 context = req.environ['meteos.context']
1235 try:
1236 resource = self._get(context, id)
1237 except exception.NotFound as e:
1238 raise webob.exc.HTTPNotFound(six.text_type(e))
1239 self._delete(context, resource, force=True)
1240 return webob.Response(status_int=202)
1241
1242
1243class Fault(webob.exc.HTTPException):
1244 """Wrap webob.exc.HTTPException to provide API friendly response."""
1245
1246 _fault_names = {400: "badRequest",
1247 401: "unauthorized",
1248 403: "forbidden",
1249 404: "itemNotFound",
1250 405: "badMethod",
1251 409: "conflictingRequest",
1252 413: "overLimit",
1253 415: "badMediaType",
1254 501: "notImplemented",
1255 503: "serviceUnavailable"}
1256
1257 def __init__(self, exception):
1258 """Create a Fault for the given webob.exc.exception."""
1259 self.wrapped_exc = exception
1260 self.status_int = exception.status_int
1261
1262 @webob.dec.wsgify(RequestClass=Request)
1263 def __call__(self, req):
1264 """Generate a WSGI response based on the exception passed to ctor."""
1265 # Replace the body with fault details.
1266 code = self.wrapped_exc.status_int
1267 fault_name = self._fault_names.get(code, "computeFault")
1268 fault_data = {
1269 fault_name: {
1270 'code': code,
1271 'message': self.wrapped_exc.explanation}}
1272 if code == 413:
1273 retry = self.wrapped_exc.headers['Retry-After']
1274 fault_data[fault_name]['retryAfter'] = retry
1275
1276 if not req.api_version_request.is_null():
1277 self.wrapped_exc.headers[API_VERSION_REQUEST_HEADER] = (
1278 req.api_version_request.get_string())
1279 if req.api_version_request.experimental:
1280 self.wrapped_exc.headers[EXPERIMENTAL_API_REQUEST_HEADER] = (
1281 req.api_version_request.experimental)
1282 self.wrapped_exc.headers['Vary'] = API_VERSION_REQUEST_HEADER
1283
1284 content_type = req.best_match_content_type()
1285 serializer = {
1286 'application/json': JSONDictSerializer(),
1287 }[content_type]
1288
1289 self.wrapped_exc.body = serializer.serialize(fault_data)
1290 self.wrapped_exc.content_type = content_type
1291 _set_request_id_header(req, self.wrapped_exc.headers)
1292
1293 return self.wrapped_exc
1294
1295 def __str__(self):
1296 return self.wrapped_exc.__str__()
1297
1298
1299def _set_request_id_header(req, headers):
1300 context = req.environ.get('meteos.context')
1301 if context:
1302 headers['x-compute-request-id'] = context.request_id
1303
1304
1305class OverLimitFault(webob.exc.HTTPException):
1306 """Rate-limited request response."""
1307
1308 def __init__(self, message, details, retry_time):
1309 """Initialize new `OverLimitFault` with relevant information."""
1310 hdrs = OverLimitFault._retry_after(retry_time)
1311 self.wrapped_exc = webob.exc.HTTPRequestEntityTooLarge(headers=hdrs)
1312 self.content = {
1313 "overLimitFault": {
1314 "code": self.wrapped_exc.status_int,
1315 "message": message,
1316 "details": details,
1317 },
1318 }
1319
1320 @staticmethod
1321 def _retry_after(retry_time):
1322 delay = int(math.ceil(retry_time - time.time()))
1323 retry_after = delay if delay > 0 else 0
1324 headers = {'Retry-After': '%d' % retry_after}
1325 return headers
1326
1327 @webob.dec.wsgify(RequestClass=Request)
1328 def __call__(self, request):
1329 """Wrap the exception.
1330
1331 Wrap the exception with a serialized body conforming to our
1332 error format.
1333 """
1334 content_type = request.best_match_content_type()
1335
1336 serializer = {
1337 'application/json': JSONDictSerializer(),
1338 }[content_type]
1339
1340 content = serializer.serialize(self.content)
1341 self.wrapped_exc.body = content
1342
1343 return self.wrapped_exc
diff --git a/meteos/api/urlmap.py b/meteos/api/urlmap.py
new file mode 100644
index 0000000..51595c7
--- /dev/null
+++ b/meteos/api/urlmap.py
@@ -0,0 +1,291 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import re
17try:
18 from urllib.request import parse_http_list # noqa
19except ImportError:
20 from urllib2 import parse_http_list # noqa
21
22import paste.urlmap
23
24from meteos.api.openstack import wsgi
25
26
27_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
28_option_header_piece_re = re.compile(
29 r';\s*([^\s;=]+|%s)\s*'
30 r'(?:=\s*([^;]+|%s))?\s*' %
31 (_quoted_string_re, _quoted_string_re))
32
33
34def unquote_header_value(value):
35 """Unquotes a header value.
36
37 This does not use the real unquoting but what browsers are actually
38 using for quoting.
39
40 :param value: the header value to unquote.
41 """
42 if value and value[0] == value[-1] == '"':
43 # this is not the real unquoting, but fixing this so that the
44 # RFC is met will result in bugs with internet explorer and
45 # probably some other browsers as well. IE for example is
46 # uploading files with "C:\foo\bar.txt" as filename
47 value = value[1:-1]
48 return value
49
50
51def parse_list_header(value):
52 """Parse lists as described by RFC 2068 Section 2.
53
54 In particular, parse comma-separated lists where the elements of
55 the list may include quoted-strings. A quoted-string could
56 contain a comma. A non-quoted string could have quotes in the
57 middle. Quotes are removed automatically after parsing.
58
59 The return value is a standard :class:`list`:
60
61 >>> parse_list_header('token, "quoted value"')
62 ['token', 'quoted value']
63
64 :param value: a string with a list header.
65 :return: :class:`list`
66 """
67 result = []
68 for item in parse_http_list(value):
69 if item[:1] == item[-1:] == '"':
70 item = unquote_header_value(item[1:-1])
71 result.append(item)
72 return result
73
74
75def parse_options_header(value):
76 """Parse header into content type and options.
77
78 Parse a ``Content-Type`` like header into a tuple with the content
79 type and the options:
80
81 >>> parse_options_header('Content-Type: text/html; mimetype=text/html')
82 ('Content-Type:', {'mimetype': 'text/html'})
83
84 :param value: the header to parse.
85 :return: (str, options)
86 """
87 def _tokenize(string):
88 for match in _option_header_piece_re.finditer(string):
89 key, value = match.groups()
90 key = unquote_header_value(key)
91 if value is not None:
92 value = unquote_header_value(value)
93 yield key, value
94
95 if not value:
96 return '', {}
97
98 parts = _tokenize(';' + value)
99 name = next(parts)[0]
100 extra = dict(parts)
101 return name, extra
102
103
104class Accept(object):
105 def __init__(self, value):
106 self._content_types = [parse_options_header(v) for v in
107 parse_list_header(value)]
108
109 def best_match(self, supported_content_types):
110 # FIXME: Should we have a more sophisticated matching algorithm that
111 # takes into account the version as well?
112 best_quality = -1
113 best_content_type = None
114 best_params = {}
115 best_match = '*/*'
116
117 for content_type in supported_content_types:
118 for content_mask, params in self._content_types:
119 try:
120 quality = float(params.get('q', 1))
121 except ValueError:
122 continue
123
124 if quality < best_quality:
125 continue
126 elif best_quality == quality:
127 if best_match.count('*') <= content_mask.count('*'):
128 continue
129
130 if self._match_mask(content_mask, content_type):
131 best_quality = quality
132 best_content_type = content_type
133 best_params = params
134 best_match = content_mask
135
136 return best_content_type, best_params
137
138 def content_type_params(self, best_content_type):
139 """Find parameters in Accept header for given content type."""
140 for content_type, params in self._content_types:
141 if best_content_type == content_type:
142 return params
143
144 return {}
145
146 def _match_mask(self, mask, content_type):
147 if '*' not in mask:
148 return content_type == mask
149 if mask == '*/*':
150 return True
151 mask_major = mask[:-2]
152 content_type_major = content_type.split('/', 1)[0]
153 return content_type_major == mask_major
154
155
156def urlmap_factory(loader, global_conf, **local_conf):
157 if 'not_found_app' in local_conf:
158 not_found_app = local_conf.pop('not_found_app')
159 else:
160 not_found_app = global_conf.get('not_found_app')
161 if not_found_app:
162 not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
163 urlmap = URLMap(not_found_app=not_found_app)
164 for path, app_name in local_conf.items():
165 path = paste.urlmap.parse_path_expression(path)
166 app = loader.get_app(app_name, global_conf=global_conf)
167 urlmap[path] = app
168 return urlmap
169
170
171class URLMap(paste.urlmap.URLMap):
172 def _match(self, host, port, path_info):
173 """Find longest match for a given URL path."""
174 for (domain, app_url), app in self.applications:
175 if domain and domain != host and domain != host + ':' + port:
176 continue
177 if (path_info == app_url or path_info.startswith(app_url + '/')):
178 return app, app_url
179
180 return None, None
181
182 def _set_script_name(self, app, app_url):
183 def wrap(environ, start_response):
184 environ['SCRIPT_NAME'] += app_url
185 return app(environ, start_response)
186
187 return wrap
188
189 def _munge_path(self, app, path_info, app_url):
190 def wrap(environ, start_response):
191 environ['SCRIPT_NAME'] += app_url
192 environ['PATH_INFO'] = path_info[len(app_url):]
193 return app(environ, start_response)
194
195 return wrap
196
197 def _path_strategy(self, host, port, path_info):
198 """Check path suffix for MIME type and path prefix for API version."""
199 mime_type = app = app_url = None
200
201 parts = path_info.rsplit('.', 1)
202 if len(parts) > 1:
203 possible_type = 'application/' + parts[1]
204 if possible_type in wsgi.SUPPORTED_CONTENT_TYPES:
205 mime_type = possible_type
206
207 parts = path_info.split('/')
208 if len(parts) > 1:
209 possible_app, possible_app_url = self._match(host, port, path_info)
210 # Don't use prefix if it ends up matching default
211 if possible_app and possible_app_url:
212 app_url = possible_app_url
213 app = self._munge_path(possible_app, path_info, app_url)
214
215 return mime_type, app, app_url
216
217 def _content_type_strategy(self, host, port, environ):
218 """Check Content-Type header for API version."""
219 app = None
220 params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1]
221 if 'version' in params:
222 app, app_url = self._match(host, port, '/v' + params['version'])
223 if app:
224 app = self._set_script_name(app, app_url)
225
226 return app
227
228 def _accept_strategy(self, host, port, environ, supported_content_types):
229 """Check Accept header for best matching MIME type and API version."""
230 accept = Accept(environ.get('HTTP_ACCEPT', ''))
231
232 app = None
233
234 # Find the best match in the Accept header
235 mime_type, params = accept.best_match(supported_content_types)
236 if 'version' in params:
237 app, app_url = self._match(host, port, '/v' + params['version'])
238 if app:
239 app = self._set_script_name(app, app_url)
240
241 return mime_type, app
242
243 def __call__(self, environ, start_response):
244 host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
245 if ':' in host:
246 host, port = host.split(':', 1)
247 else:
248 if environ['wsgi.url_scheme'] == 'http':
249 port = '80'
250 else:
251 port = '443'
252
253 path_info = environ['PATH_INFO']
254 path_info = self.normalize_url(path_info, False)[1]
255
256 # The API version is determined in one of three ways:
257 # 1) URL path prefix (eg /v1.1/tenant/servers/detail)
258 # 2) Content-Type header (eg application/json;version=1.1)
259 # 3) Accept header (eg application/json;q=0.8;version=1.1)
260
261 # Meteos supports only application/json as MIME type for the responses.
262 supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES)
263
264 mime_type, app, app_url = self._path_strategy(host, port, path_info)
265
266 if not app:
267 app = self._content_type_strategy(host, port, environ)
268
269 if not mime_type or not app:
270 possible_mime_type, possible_app = self._accept_strategy(
271 host, port, environ, supported_content_types)
272 if possible_mime_type and not mime_type:
273 mime_type = possible_mime_type
274 if possible_app and not app:
275 app = possible_app
276
277 if not mime_type:
278 mime_type = 'application/json'
279
280 if not app:
281 # Didn't match a particular version, probably matches default
282 app, app_url = self._match(host, port, path_info)
283 if app:
284 app = self._munge_path(app, path_info, app_url)
285
286 if app:
287 environ['meteos.best_content_type'] = mime_type
288 return app(environ, start_response)
289
290 environ['paste.urlmap_object'] = self
291 return self.not_found_application(environ, start_response)
diff --git a/meteos/api/v1/__init__.py b/meteos/api/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meteos/api/v1/__init__.py
diff --git a/meteos/api/v1/datasets.py b/meteos/api/v1/datasets.py
new file mode 100644
index 0000000..07cf8bc
--- /dev/null
+++ b/meteos/api/v1/datasets.py
@@ -0,0 +1,156 @@
1# Copyright 2013 NetApp
2# All Rights Reserved.
3# Copyright (c) 2016 NEC Corporation.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17"""The datasets api."""
18
19import ast
20import re
21import string
22
23from oslo_log import log
24from oslo_utils import strutils
25from oslo_utils import uuidutils
26import six
27import webob
28from webob import exc
29
30from meteos.api import common
31from meteos.api.openstack import wsgi
32from meteos.api.views import datasets as dataset_views
33from meteos import exception
34from meteos.i18n import _, _LI
35from meteos import engine
36
37LOG = log.getLogger(__name__)
38
39
40class DatasetController(wsgi.Controller, wsgi.AdminActionsMixin):
41
42 """The Datasets API v1 controller for the OpenStack API."""
43 resource_name = 'dataset'
44 _view_builder_class = dataset_views.ViewBuilder
45
46 def __init__(self):
47 super(self.__class__, self).__init__()
48 self.engine_api = engine.API()
49
50 def show(self, req, id):
51 """Return data about the given dataset."""
52 context = req.environ['meteos.context']
53
54 try:
55 dataset = self.engine_api.get_dataset(context, id)
56 except exception.NotFound:
57 raise exc.HTTPNotFound()
58
59 return self._view_builder.detail(req, dataset)
60
61 def delete(self, req, id):
62 """Delete a dataset."""
63 context = req.environ['meteos.context']
64
65 LOG.info(_LI("Delete dataset with id: %s"), id, context=context)
66
67 try:
68 self.engine_api.delete_dataset(context, id)
69 except exception.NotFound:
70 raise exc.HTTPNotFound()
71 except exception.InvalidLearning as e:
72 raise exc.HTTPForbidden(explanation=six.text_type(e))
73
74 return webob.Response(status_int=202)
75
76 def index(self, req):
77 """Returns a summary list of datasets."""
78 return self._get_datasets(req, is_detail=False)
79
80 def detail(self, req):
81 """Returns a detailed list of datasets."""
82 return self._get_datasets(req, is_detail=True)
83
84 def _get_datasets(self, req, is_detail):
85 """Returns a list of datasets, transformed through view builder."""
86 context = req.environ['meteos.context']
87
88 search_opts = {}
89 search_opts.update(req.GET)
90
91 # Remove keys that are not related to dataset attrs
92 search_opts.pop('limit', None)
93 search_opts.pop('offset', None)
94 sort_key = search_opts.pop('sort_key', 'created_at')
95 sort_dir = search_opts.pop('sort_dir', 'desc')
96
97 datasets = self.engine_api.get_all_datasets(
98 context, search_opts=search_opts, sort_key=sort_key,
99 sort_dir=sort_dir)
100
101 limited_list = common.limited(datasets, req)
102
103 if is_detail:
104 datasets = self._view_builder.detail_list(req, limited_list)
105 else:
106 datasets = self._view_builder.summary_list(req, limited_list)
107 return datasets
108
109 def create(self, req, body):
110 """Creates a new dataset."""
111 context = req.environ['meteos.context']
112
113 if not self.is_valid_body(body, 'dataset'):
114 raise exc.HTTPUnprocessableEntity()
115
116 dataset = body['dataset']
117
118 LOG.debug("Create dataset with request: %s", dataset)
119
120 try:
121 experiment = self.engine_api.get_experiment(
122 context, dataset['experiment_id'])
123 template = self.engine_api.get_template(
124 context, experiment.template_id)
125 except exception.NotFound:
126 raise exc.HTTPNotFound()
127
128 display_name = dataset.get('display_name')
129 display_description = dataset.get('display_description')
130 method = dataset.get('method')
131 experiment_id = dataset.get('experiment_id')
132 source_dataset_url = dataset.get('source_dataset_url')
133 params = dataset.get('params')
134 swift_tenant = dataset.get('swift_tenant')
135 swift_username = dataset.get('swift_username')
136 swift_password = dataset.get('swift_password')
137
138 new_dataset = self.engine_api.create_dataset(context,
139 display_name,
140 display_description,
141 method,
142 source_dataset_url,
143 params,
144 template.id,
145 template.job_template_id,
146 experiment_id,
147 experiment.cluster_id,
148 swift_tenant,
149 swift_username,
150 swift_password)
151
152 return self._view_builder.detail(req, new_dataset)
153
154
155def create_resource():
156 return wsgi.Resource(DatasetController())
diff --git a/meteos/api/v1/experiments.py b/meteos/api/v1/experiments.py
new file mode 100644
index 0000000..0cd6958
--- /dev/null
+++ b/meteos/api/v1/experiments.py
@@ -0,0 +1,144 @@
1# Copyright 2013 NetApp
2# All Rights Reserved.
3# Copyright (c) 2016 NEC Corporation.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17"""The experiments api."""
18
19import ast
20import re
21import string
22
23from oslo_log import log
24from oslo_utils import strutils
25from oslo_utils import uuidutils
26import six
27import webob
28from webob import exc
29
30from meteos.api import common
31from meteos.api.openstack import wsgi
32from meteos.api.views import experiments as experiment_views
33from meteos import exception
34from meteos.i18n import _, _LI
35from meteos import engine
36
37LOG = log.getLogger(__name__)
38
39
40class ExperimentController(wsgi.Controller, wsgi.AdminActionsMixin):
41
42 """The Experiments API v1 controller for the OpenStack API."""
43 resource_name = 'experiment'
44 _view_builder_class = experiment_views.ViewBuilder
45
46 def __init__(self):
47 super(self.__class__, self).__init__()