Browse Source

Initial commit

Change-Id: I0ebc7a689a37b077606545d50a5d139f2059ffdb
changes/88/408588/1
Hiroyuki Eguchi 2 years ago
parent
commit
1491d54a1d
100 changed files with 13023 additions and 0 deletions
  1. 19
    0
      CONTRIBUTING.rst
  2. 11
    0
      HACKING.rst
  3. 176
    0
      LICENSE
  4. 27
    0
      README.rst
  5. 2
    0
      babel.cfg
  6. 21
    0
      devstack/README.rst
  7. 50
    0
      devstack/exercise.sh
  8. 187
    0
      devstack/plugin.sh
  9. 37
    0
      devstack/settings
  10. 4
    0
      etc/meteos/README.meteos.conf
  11. 49
    0
      etc/meteos/api-paste.ini
  12. 49
    0
      etc/meteos/api-paste.ini.orig
  13. 73
    0
      etc/meteos/logging_sample.conf
  14. 39
    0
      etc/meteos/meteos.conf
  15. 13
    0
      etc/meteos/policy.json
  16. 27
    0
      etc/meteos/rootwrap.conf
  17. 20
    0
      etc/meteos/rootwrap.d/learning.filters
  18. 9
    0
      etc/oslo-config-generator/meteos.conf
  19. 0
    0
      meteos/__init__.py
  20. 21
    0
      meteos/api/__init__.py
  21. 38
    0
      meteos/api/auth.py
  22. 318
    0
      meteos/api/common.py
  23. 37
    0
      meteos/api/contrib/__init__.py
  24. 0
    0
      meteos/api/middleware/__init__.py
  25. 150
    0
      meteos/api/middleware/auth.py
  26. 74
    0
      meteos/api/middleware/fault.py
  27. 81
    0
      meteos/api/openstack/__init__.py
  28. 169
    0
      meteos/api/openstack/api_version_request.py
  29. 14
    0
      meteos/api/openstack/rest_api_version_history.rst
  30. 29
    0
      meteos/api/openstack/urlmap.py
  31. 49
    0
      meteos/api/openstack/versioned_method.py
  32. 1343
    0
      meteos/api/openstack/wsgi.py
  33. 291
    0
      meteos/api/urlmap.py
  34. 0
    0
      meteos/api/v1/__init__.py
  35. 156
    0
      meteos/api/v1/datasets.py
  36. 144
    0
      meteos/api/v1/experiments.py
  37. 155
    0
      meteos/api/v1/learnings.py
  38. 158
    0
      meteos/api/v1/models.py
  39. 73
    0
      meteos/api/v1/router.py
  40. 145
    0
      meteos/api/v1/templates.py
  41. 97
    0
      meteos/api/versions.py
  42. 0
    0
      meteos/api/views/__init__.py
  43. 80
    0
      meteos/api/views/datasets.py
  44. 80
    0
      meteos/api/views/experiments.py
  45. 84
    0
      meteos/api/views/learnings.py
  46. 82
    0
      meteos/api/views/models.py
  47. 85
    0
      meteos/api/views/templates.py
  48. 66
    0
      meteos/api/views/versions.py
  49. 33
    0
      meteos/cluster/__init__.py
  50. 386
    0
      meteos/cluster/binary/meteos-script-1.6.0.py
  51. 177
    0
      meteos/cluster/sahara.py
  52. 0
    0
      meteos/cmd/__init__.py
  53. 54
    0
      meteos/cmd/api.py
  54. 60
    0
      meteos/cmd/engine.py
  55. 465
    0
      meteos/cmd/manage.py
  56. 0
    0
      meteos/common/__init__.py
  57. 107
    0
      meteos/common/client_auth.py
  58. 178
    0
      meteos/common/config.py
  59. 28
    0
      meteos/common/constants.py
  60. 151
    0
      meteos/context.py
  61. 20
    0
      meteos/db/__init__.py
  62. 328
    0
      meteos/db/api.py
  63. 37
    0
      meteos/db/base.py
  64. 48
    0
      meteos/db/migration.py
  65. 0
    0
      meteos/db/migrations/__init__.py
  66. 59
    0
      meteos/db/migrations/alembic.ini
  67. 0
    0
      meteos/db/migrations/alembic/__init__.py
  68. 41
    0
      meteos/db/migrations/alembic/env.py
  69. 84
    0
      meteos/db/migrations/alembic/migration.py
  70. 34
    0
      meteos/db/migrations/alembic/script.py.mako
  71. 228
    0
      meteos/db/migrations/alembic/versions/001_meteos_init.py
  72. 21
    0
      meteos/db/migrations/utils.py
  73. 0
    0
      meteos/db/sqlalchemy/__init__.py
  74. 915
    0
      meteos/db/sqlalchemy/api.py
  75. 200
    0
      meteos/db/sqlalchemy/models.py
  76. 40
    0
      meteos/db/sqlalchemy/query.py
  77. 25
    0
      meteos/engine/__init__.py
  78. 470
    0
      meteos/engine/api.py
  79. 81
    0
      meteos/engine/configuration.py
  80. 127
    0
      meteos/engine/driver.py
  81. 22
    0
      meteos/engine/drivers/__init__.py
  82. 468
    0
      meteos/engine/drivers/generic.py
  83. 340
    0
      meteos/engine/manager.py
  84. 118
    0
      meteos/engine/rpcapi.py
  85. 194
    0
      meteos/exception.py
  86. 0
    0
      meteos/hacking/__init__.py
  87. 360
    0
      meteos/hacking/checks.py
  88. 50
    0
      meteos/i18n.py
  89. 114
    0
      meteos/manager.py
  90. 75
    0
      meteos/opts.py
  91. 113
    0
      meteos/policy.py
  92. 153
    0
      meteos/rpc.py
  93. 379
    0
      meteos/service.py
  94. 354
    0
      meteos/test.py
  95. 53
    0
      meteos/testing/README.rst
  96. 27
    0
      meteos/tests/__init__.py
  97. 400
    0
      meteos/utils.py
  98. 23
    0
      meteos/version.py
  99. 551
    0
      meteos/wsgi.py
  100. 0
    0
      pylintrc

+ 19
- 0
CONTRIBUTING.rst View File

@@ -0,0 +1,19 @@
1
+If you would like to contribute to the development of OpenStack,
2
+you must follow the steps in this page:
3
+
4
+   http://docs.openstack.org/infra/manual/developers.html
5
+
6
+Once those steps have been completed, changes to OpenStack
7
+should be submitted for review via the Gerrit tool, following
8
+the workflow documented at:
9
+
10
+   http://docs.openstack.org/infra/manual/developers.html#development-workflow
11
+
12
+Pull requests submitted through GitHub will be ignored.
13
+
14
+Bugs should be filed on Launchpad, not GitHub:
15
+
16
+   https://bugs.launchpad.net/meteos
17
+
18
+
19
+

+ 11
- 0
HACKING.rst View File

@@ -0,0 +1,11 @@
1
+Meteos 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
+
9
+Meteos Specific Commandments
10
+-------------------------------
11
+

+ 176
- 0
LICENSE View File

@@ -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
+

+ 27
- 0
README.rst View File

@@ -0,0 +1,27 @@
1
+======
2
+Meteos
3
+======
4
+
5
+You have come across an OpenStack Machine Learning service.  It has
6
+identified itself as "Meteos."  It was abstracted from the Manila
7
+project.
8
+
9
+* Wiki: https://wiki.openstack.org/Meteos
10
+* Developer docs: http://docs.openstack.org/developer/meteos
11
+
12
+Getting Started
13
+---------------
14
+
15
+If 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
+
19
+For developer information please see
20
+`HACKING.rst <https://github.com/openstack/meteos/blob/master/HACKING.rst>`_
21
+
22
+You can raise bugs here http://bugs.launchpad.net/meteos
23
+
24
+Python client
25
+-------------
26
+
27
+https://github.com/openstack/python-meteosclient.git

+ 2
- 0
babel.cfg View File

@@ -0,0 +1,2 @@
1
+[python: **.py]
2
+

+ 21
- 0
devstack/README.rst View File

@@ -0,0 +1,21 @@
1
+======================
2
+ Enabling in Devstack
3
+======================
4
+
5
+1. Download DevStack
6
+
7
+2. 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
+
14
+Optionally, 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
+
21
+3. run ``stack.sh``

+ 50
- 0
devstack/exercise.sh View File

@@ -0,0 +1,50 @@
1
+#!/usr/bin/env bash
2
+
3
+# Sanity check that Meteos started if enabled
4
+
5
+echo "*********************************************************************"
6
+echo "Begin DevStack Exercise: $0"
7
+echo "*********************************************************************"
8
+
9
+# This script exits on an error so that errors don't compound and you see
10
+# only the first error that occurred.
11
+set -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.
15
+set -o xtrace
16
+
17
+
18
+# Settings
19
+# ========
20
+
21
+# Keep track of the current directory
22
+EXERCISE_DIR=$(cd $(dirname "$0") && pwd)
23
+TOP_DIR=$(cd $EXERCISE_DIR/..; pwd)
24
+
25
+# Import common functions
26
+source $TOP_DIR/functions
27
+
28
+# Import configuration
29
+source $TOP_DIR/openrc
30
+
31
+# Import exercise configuration
32
+source $TOP_DIR/exerciserc
33
+
34
+is_service_enabled meteos || exit 55
35
+
36
+if is_ssl_enabled_service "meteos" ||\
37
+    is_ssl_enabled_service "meteos-api" ||\
38
+    is_service_enabled tls-proxy; then
39
+    METEOS_SERVICE_PROTOCOL="https"
40
+fi
41
+
42
+METEOS_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
+
47
+set +o xtrace
48
+echo "*********************************************************************"
49
+echo "SUCCESS: End DevStack Exercise: $0"
50
+echo "*********************************************************************"

+ 187
- 0
devstack/plugin.sh View File

@@ -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
19
+XTRACE=$(set +o | grep xtrace)
20
+set +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
31
+function 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.
45
+function 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
52
+function 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
96
+function install_meteos {
97
+    setup_develop $METEOS_DIR
98
+}
99
+
100
+# install_python_meteosclient() - Collect source and prepare
101
+function 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
107
+function 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
126
+function 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
134
+function 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.
143
+function 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
154
+if 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
179
+fi
180
+
181
+
182
+# Restore xtrace
183
+$XTRACE
184
+
185
+# Local variables:
186
+# mode: shell-script
187
+# End:

+ 37
- 0
devstack/settings View File

@@ -0,0 +1,37 @@
1
+#!/bin/bash
2
+
3
+# Settings needed for the Meteos plugin
4
+# -------------------------------------
5
+
6
+# Set up default directories
7
+METEOSCLIENT_DIR=$DEST/python-meteosclient
8
+METEOS_DIR=$DEST/meteos
9
+
10
+METEOSCLIENT_REPO=${METEOSCLIENT_REPO:-\
11
+${GIT_BASE}/openstack/python-meteosclient.git}
12
+METEOSCLIENT_BRANCH=${METEOSCLIENT_BRANCH:-master}
13
+
14
+METEOS_CONF_DIR=${METEOS_CONF_DIR:-/etc/meteos}
15
+METEOS_CONF_FILE=${METEOS_CONF_DIR}/meteos.conf
16
+
17
+# TODO(slukjanov): Should we append meteos to SSL_ENABLED_SERVICES?
18
+
19
+if is_ssl_enabled_service "meteos" || is_service_enabled tls-proxy; then
20
+    METEOS_SERVICE_PROTOCOL="https"
21
+fi
22
+METEOS_SERVICE_HOST=${METEOS_SERVICE_HOST:-$SERVICE_HOST}
23
+METEOS_SERVICE_PORT=${METEOS_SERVICE_PORT:-8989}
24
+METEOS_SERVICE_PROTOCOL=${METEOS_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
25
+METEOS_ENDPOINT_TYPE=${METEOS_ENDPOINT_TYPE:-adminURL}
26
+
27
+METEOS_AUTH_CACHE_DIR=${METEOS_AUTH_CACHE_DIR:-/var/cache/meteos}
28
+
29
+METEOS_BIN_DIR=$(get_python_exec_prefix)
30
+
31
+METEOS_ENABLE_DISTRIBUTED_PERIODICS=${METEOS_ENABLE_DISTRIBUTED_PERIODICS:-\
32
+True}
33
+
34
+# Tell Tempest this project is present
35
+TEMPEST_SERVICES+=,meteos
36
+
37
+enable_service meteos-api meteos-eng

+ 4
- 0
etc/meteos/README.meteos.conf View File

@@ -0,0 +1,4 @@
1
+To generate the sample meteos.conf file, run the following
2
+command from the top level of the meteos directory:
3
+
4
+tox -egenconfig

+ 49
- 0
etc/meteos/api-paste.ini View File

@@ -0,0 +1,49 @@
1
+#############
2
+# OpenStack #
3
+#############
4
+
5
+[composite:osapi_learning]
6
+use = call:meteos.api:root_app_factory
7
+/: apiversions
8
+/v1: openstack_learning_api
9
+
10
+[composite:openstack_learning_api]
11
+use = call:meteos.api.middleware.auth:pipeline_factory
12
+noauth = cors faultwrap http_proxy_to_wsgi sizelimit noauth api
13
+keystone = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
14
+keystone_nolimit = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
15
+
16
+[filter:faultwrap]
17
+paste.filter_factory = meteos.api.middleware.fault:FaultWrapper.factory
18
+
19
+[filter:noauth]
20
+paste.filter_factory = meteos.api.middleware.auth:NoAuthMiddleware.factory
21
+
22
+[filter:sizelimit]
23
+paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
24
+
25
+[filter:http_proxy_to_wsgi]
26
+paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
27
+
28
+[app:api]
29
+paste.app_factory = meteos.api.v1.router:APIRouter.factory
30
+
31
+[pipeline:apiversions]
32
+pipeline = cors faultwrap http_proxy_to_wsgi oslearningversionapp
33
+
34
+[app:oslearningversionapp]
35
+paste.app_factory = meteos.api.versions:VersionsRouter.factory
36
+
37
+##########
38
+# Engine #
39
+##########
40
+
41
+[filter:keystonecontext]
42
+paste.filter_factory = meteos.api.middleware.auth:MeteosKeystoneContext.factory
43
+
44
+[filter:authtoken]
45
+paste.filter_factory = keystonemiddleware.auth_token:filter_factory
46
+
47
+[filter:cors]
48
+paste.filter_factory = oslo_middleware.cors:filter_factory
49
+oslo_config_project = meteos

+ 49
- 0
etc/meteos/api-paste.ini.orig View File

@@ -0,0 +1,49 @@
1
+#############
2
+# OpenStack #
3
+#############
4
+
5
+[composite:osapi_learning]
6
+use = call:meteos.api:root_app_factory
7
+/: apiversions
8
+/v1: openstack_learning_api
9
+
10
+[composite:openstack_learning_api]
11
+use = call:meteos.api.middleware.auth:pipeline_factory
12
+noauth = cors faultwrap http_proxy_to_wsgi sizelimit noauth api
13
+keystone = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
14
+keystone_nolimit = cors faultwrap http_proxy_to_wsgi sizelimit authtoken keystonecontext api
15
+
16
+[filter:faultwrap]
17
+paste.filter_factory = meteos.api.middleware.fault:FaultWrapper.factory
18
+
19
+[filter:noauth]
20
+paste.filter_factory = meteos.api.middleware.auth:NoAuthMiddleware.factory
21
+
22
+[filter:sizelimit]
23
+paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
24
+
25
+[filter:http_proxy_to_wsgi]
26
+paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
27
+
28
+[app:api]
29
+paste.app_factory = meteos.api.v1.router:APIRouter.factory
30
+
31
+[pipeline:apiversions]
32
+pipeline = cors faultwrap http_proxy_to_wsgi oslearningversionapp
33
+
34
+[app:oslearningversionapp]
35
+paste.app_factory = meteos.api.versions:VersionsRouter.factory
36
+
37
+##########
38
+# Engine #
39
+##########
40
+
41
+[filter:keystonecontext]
42
+paste.filter_factory = meteos.api.middleware.auth:MeteosKeystoneContext.factory
43
+
44
+[filter:authtoken]
45
+paste.filter_factory = keystonemiddleware.auth_token:filter_factory
46
+
47
+[filter:cors]
48
+paste.filter_factory = oslo_middleware.cors:filter_factory
49
+oslo_config_project = meteos

+ 73
- 0
etc/meteos/logging_sample.conf View File

@@ -0,0 +1,73 @@
1
+[loggers]
2
+keys = root, meteos
3
+
4
+[handlers]
5
+keys = stderr, stdout, watchedfile, syslog, null
6
+
7
+[formatters]
8
+keys = default
9
+
10
+[logger_root]
11
+level = WARNING
12
+handlers = null
13
+
14
+[logger_meteos]
15
+level = INFO
16
+handlers = stderr
17
+qualname = meteos
18
+
19
+[logger_amqplib]
20
+level = WARNING
21
+handlers = stderr
22
+qualname = amqplib
23
+
24
+[logger_sqlalchemy]
25
+level = WARNING
26
+handlers = stderr
27
+qualname = 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]
33
+level = WARNING
34
+handlers = stderr
35
+qualname = boto
36
+
37
+[logger_suds]
38
+level = INFO
39
+handlers = stderr
40
+qualname = suds
41
+
42
+[logger_eventletwsgi]
43
+level = WARNING
44
+handlers = stderr
45
+qualname = eventlet.wsgi.server
46
+
47
+[handler_stderr]
48
+class = StreamHandler
49
+args = (sys.stderr,)
50
+formatter = default
51
+
52
+[handler_stdout]
53
+class = StreamHandler
54
+args = (sys.stdout,)
55
+formatter = default
56
+
57
+[handler_watchedfile]
58
+class = handlers.WatchedFileHandler
59
+args = ('meteos.log',)
60
+formatter = default
61
+
62
+[handler_syslog]
63
+class = handlers.SysLogHandler
64
+args = ('/dev/log', handlers.SysLogHandler.LOG_USER)
65
+formatter = default
66
+
67
+[handler_null]
68
+class = meteos.common.openstack.NullHandler
69
+formatter = default
70
+args = ()
71
+
72
+[formatter_default]
73
+format = %(message)s

+ 39
- 0
etc/meteos/meteos.conf View File

@@ -0,0 +1,39 @@
1
+[DEFAULT]
2
+
3
+debug = True
4
+verbose = True
5
+log_dir = /var/log/meteos
6
+rpc_backend = rabbit
7
+
8
+host = 10.0.0.6
9
+hostname = meteos-vm
10
+osapi_learning_listen_port = 8989
11
+osapi_learning_workers = 4
12
+meteos-engine_workers = 4
13
+api_paste_config=api-paste.ini
14
+
15
+[sahara]
16
+auth_url = http://192.168.0.4:5000/v2.0
17
+
18
+[database]
19
+connection = sqlite:///etc/meteos/meteos.sqlite3
20
+
21
+[keystone_authtoken]
22
+
23
+auth_uri = http://192.168.0.4:5000/v2.0
24
+identity_uri=http://192.168.0.4:35357
25
+admin_tenant_name=services
26
+admin_password=9396d9e9e57545d8
27
+admin_user=sahara
28
+
29
+[oslo_messaging_rabbit]
30
+
31
+rabbit_host = 192.168.0.4
32
+rabbit_port = 5672
33
+rabbit_use_ssl = False
34
+rabbit_userid = guest
35
+rabbit_password = guest
36
+
37
+[engine]
38
+
39
+worker = 4

+ 13
- 0
etc/meteos/policy.json View File

@@ -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
+}

+ 27
- 0
etc/meteos/rootwrap.conf View File

@@ -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 !
7
+filters_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 !
13
+exec_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
17
+use_syslog=False
18
+
19
+# Which syslog facility to use.
20
+# Valid values include auth, authpriv, syslog, user0, user1...
21
+# Default value is 'syslog'
22
+syslog_log_facility=syslog
23
+
24
+# Which messages to log.
25
+# INFO means log all usage
26
+# ERROR means only log unsuccessful attempts
27
+syslog_log_level=ERROR

+ 20
- 0
etc/meteos/rootwrap.d/learning.filters View File

@@ -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'
6
+chown: CommandFilter, chown, root
7
+# meteos/utils.py : 'cat', '%s'
8
+cat: CommandFilter, cat, root
9
+
10
+# meteos/share/drivers/lvm.py: 'rmdir', '%s'
11
+rmdir: CommandFilter, rmdir, root
12
+
13
+# meteos/share/drivers/helpers.py: 'cp', '%s', '%s'
14
+cp: CommandFilter, cp, root
15
+
16
+# meteos/share/drivers/helpers.py: 'service', '%s', '%s'
17
+service: CommandFilter, service, root
18
+
19
+# meteos/share/drivers/glusterfs.py: 'rm', '-rf', '%s'
20
+rm: CommandFilter, rm, root

+ 9
- 0
etc/oslo-config-generator/meteos.conf View File

@@ -0,0 +1,9 @@
1
+[DEFAULT]
2
+output_file = etc/meteos/meteos.conf.sample
3
+namespace = meteos
4
+namespace = oslo.messaging
5
+namespace = oslo.middleware.cors
6
+namespace = oslo.middleware.http_proxy_to_wsgi
7
+namespace = oslo.db
8
+namespace = oslo.db.concurrency
9
+namespace = keystonemiddleware.auth_token

+ 0
- 0
meteos/__init__.py View File


+ 21
- 0
meteos/api/__init__.py View File

@@ -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
+
17
+import paste.urlmap
18
+
19
+
20
+def root_app_factory(loader, global_conf, **local_conf):
21
+    return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)

+ 38
- 0
meteos/api/auth.py View File

@@ -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
+
17
+from oslo_log import log
18
+
19
+from meteos.api.middleware import auth
20
+from meteos.i18n import _LW
21
+
22
+LOG = log.getLogger(__name__)
23
+
24
+
25
+class 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
+
34
+def 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)

+ 318
- 0
meteos/api/common.py View File

@@ -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
+
16
+import os
17
+import re
18
+
19
+from oslo_config import cfg
20
+from oslo_log import log
21
+from six.moves.urllib import parse
22
+import webob
23
+
24
+from meteos.api.openstack import api_version_request as api_version
25
+from meteos.api.openstack import versioned_method
26
+from meteos.i18n import _
27
+
28
+api_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
+
39
+CONF = cfg.CONF
40
+CONF.register_opts(api_common_opts)
41
+LOG = 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
49
+VALID_KEY_NAME_REGEX = re.compile(r"^[\w\.\-\:\_]+$", re.UNICODE)
50
+
51
+
52
+def 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
+
60
+def 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
+
80
+def _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
+
93
+def _get_marker_param(request):
94
+    """Extract marker ID from request or fail."""
95
+    return request.GET['marker']
96
+
97
+
98
+def 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
+
135
+def 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
+
161
+def 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
+
191
+def 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
+
201
+class 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
+
306
+def 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]

+ 37
- 0
meteos/api/contrib/__init__.py View File

@@ -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
+
18
+It can't be called 'extensions' because that causes namespacing problems.
19
+
20
+"""
21
+
22
+from oslo_config import cfg
23
+from oslo_log import log
24
+
25
+from meteos.api import extensions
26
+
27
+CONF = cfg.CONF
28
+LOG = log.getLogger(__name__)
29
+
30
+
31
+def standard_extensions(ext_mgr):
32
+    extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__)
33
+
34
+
35
+def select_extensions(ext_mgr):
36
+    extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__,
37
+                                        CONF.osapi_learning_ext_list)

+ 0
- 0
meteos/api/middleware/__init__.py View File


+ 150
- 0
meteos/api/middleware/auth.py View File

@@ -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
+"""
16
+Common Auth Middleware.
17
+
18
+"""
19
+import os
20
+
21
+from oslo_config import cfg
22
+from oslo_log import log
23
+from oslo_serialization import jsonutils
24
+import webob.dec
25
+import webob.exc
26
+
27
+from meteos.api.openstack import wsgi
28
+from meteos import context
29
+from meteos.i18n import _
30
+from meteos import wsgi as base_wsgi
31
+
32
+use_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
+
38
+CONF = cfg.CONF
39
+CONF.register_opt(use_forwarded_for_opt)
40
+LOG = log.getLogger(__name__)
41
+
42
+
43
+def 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
+
58
+class 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
+
71
+class 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
+
119
+class 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

+ 74
- 0
meteos/api/middleware/fault.py View File

@@ -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
+
17
+from oslo_log import log
18
+import six
19
+import webob.dec
20
+import webob.exc
21
+
22
+from meteos.api.openstack import wsgi
23
+from meteos.i18n import _LE, _LI
24
+from meteos import utils
25
+from meteos import wsgi as base_wsgi
26
+
27
+LOG = log.getLogger(__name__)
28
+
29
+
30
+class 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)

+ 81
- 0
meteos/api/openstack/__init__.py View File

@@ -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
+"""
18
+WSGI middleware for OpenStack API controllers.
19
+"""
20
+
21
+from oslo_log import log
22
+import routes
23
+
24
+from meteos.api.openstack import wsgi
25
+from meteos.i18n import _, _LW
26
+from meteos import wsgi as base_wsgi
27
+
28
+LOG = log.getLogger(__name__)
29
+
30
+
31
+class 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
+
39
+class 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
+
55
+class 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
+
74
+class 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))

+ 169
- 0
meteos/api/openstack/api_version_request.py View File

@@ -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
+
17
+import re
18
+
19
+from meteos.api.openstack import versioned_method
20
+from meteos import exception
21
+from meteos.i18n import _
22
+from 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
43
+REST_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"
55
+DEFAULT_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.
61
+def min_api_version():
62
+    return APIVersionRequest(_MIN_API_VERSION)
63
+
64
+
65
+def max_api_version():
66
+    return APIVersionRequest(_MAX_API_VERSION)
67
+
68
+
69
+class 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})

+ 14
- 0
meteos/api/openstack/rest_api_version_history.rst View File

@@ -0,0 +1,14 @@
1
+REST API Version History
2
+========================
3
+
4
+This documents the changes made to the REST API with every
5
+microversion change. The description for each version should be a
6
+verbose one which has enough information to be suitable for use in
7
+user documentation.
8
+
9
+1.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.

+ 29
- 0
meteos/api/openstack/urlmap.py View File

@@ -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
+
17
+from oslo_log import log
18
+
19
+from meteos.api import urlmap
20
+from meteos.i18n import _LW
21
+
22
+LOG = log.getLogger(__name__)
23
+
24
+
25
+def 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)

+ 49
- 0
meteos/api/openstack/versioned_method.py View File

@@ -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
+
17
+from meteos import utils
18
+
19
+
20
+class 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

+ 1343
- 0
meteos/api/openstack/wsgi.py
File diff suppressed because it is too large
View File


+ 291
- 0
meteos/api/urlmap.py View File

@@ -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
+
16
+import re
17
+try:
18
+    from urllib.request import parse_http_list   # noqa
19
+except ImportError:
20
+    from urllib2 import parse_http_list   # noqa
21
+
22
+import paste.urlmap
23
+
24
+from 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
+
34
+def 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
+
51
+def 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
+
75
+def 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
+
104
+class 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
+
156
+def 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
+
171
+class 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)

+ 0
- 0
meteos/api/v1/__init__.py View File


+ 156
- 0
meteos/api/v1/datasets.py View File

@@ -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
+
19
+import ast
20
+import re
21
+import string
22
+
23
+from oslo_log import log
24
+from oslo_utils import strutils
25
+from oslo_utils import uuidutils
26
+import six
27
+import webob
28
+from webob import exc
29
+
30
+from meteos.api import common
31
+from meteos.api.openstack import wsgi
32
+from meteos.api.views import datasets as dataset_views
33
+from meteos import exception
34
+from meteos.i18n import _, _LI
35
+from meteos import engine
36
+
37
+LOG = log.getLogger(__name__)
38
+
39
+
40
+class 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
+
155
+def create_resource():
156
+    return wsgi.Resource(DatasetController())

+ 144
- 0
meteos/api/v1/experiments.py View File

@@ -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
+
19
+import ast
20
+import re
21
+import string
22
+
23
+from oslo_log import log
24
+from oslo_utils import strutils
25
+from oslo_utils import uuidutils
26
+import six
27
+import webob
28
+from webob import exc
29
+
30
+from meteos.api import common
31
+from meteos.api.openstack import wsgi
32
+from meteos.api.views import experiments as experiment_views
33
+from meteos import exception
34
+from meteos.i18n import _, _LI
35
+from meteos import engine
36
+
37
+LOG = log.getLogger(__name__)
38
+
39
+
40
+class 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__()
48
+        self.engine_api = engine.API()
49
+
50
+    def show(self, req, id):
51
+        """Return data about the given experiment."""
52
+        context = req.environ['meteos.context']
53
+
54
+        try:
55
+            experiment = self.engine_api.get_experiment(context, id)
56
+        except exception.NotFound:
57
+            raise exc.HTTPNotFound()
58
+
59
+        return self._view_builder.detail(req, experiment)
60
+
61
+    def delete(self, req, id):
62
+        """Delete a experiment."""
63
+        context = req.environ['meteos.context']
64
+
65
+        LOG.info(_LI("Delete experiment with id: %s"), id, context=context)
66
+
67
+        try:
68
+            self.engine_api.delete_experiment(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 experiments."""
78
+        return self._get_experiments(req, is_detail=False)
79
+
80
+    def detail(self, req):
81
+        """Returns a detailed list of experiments."""
82
+        return self._get_experiments(req, is_detail=True)
83
+
84
+    def _get_experiments(self, req, is_detail):
85
+        """Returns a list of experiments, 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 experiment 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
+        experiments = self.engine_api.get_all_experiments(
98
+            context, search_opts=search_opts, sort_key=sort_key,
99
+            sort_dir=sort_dir)
100
+
101
+        limited_list = common.limited(experiments, req)
102
+
103
+        if is_detail:
104
+            experiments = self._view_builder.detail_list(req, limited_list)
105
+        else:
106
+            experiments = self._view_builder.summary_list(req, limited_list)
107
+        return experiments
108
+
109
+    def create(self, req, body):
110
+        """Creates a new experiment."""
111
+        context = req.environ['meteos.context']
112
+
113
+        if not self.is_valid_body(body, 'experiment'):
114
+            raise exc.HTTPUnprocessableEntity()
115
+
116
+        experiment = body['experiment']
117
+
118
+        LOG.debug("Create experiment with request: %s", experiment)
119
+
120
+        try:
121
+            self.engine_api.get_template(context, experiment['template_id'])
122
+        except exception.NotFound:
123
+            raise exc.HTTPNotFound()
124
+
125
+        display_name = experiment.get('display_name')
126
+        display_description = experiment.get('display_description')
127
+        template_id = experiment.get('template_id')
128
+        key_name = experiment.get('key_name')
129
+        neutron_management_network = experiment.get(
130
+            'neutron_management_network')
131
+
132
+        new_experiment = self.engine_api.create_experiment(
133
+            context,
134
+            display_name,
135
+            display_description,
136
+            template_id,
137
+            key_name,
138
+            neutron_management_network)
139
+
140
+        return self._view_builder.detail(req, new_experiment)
141
+
142
+
143
+def create_resource():
144
+    return wsgi.Resource(ExperimentController())

+ 155
- 0
meteos/api/v1/learnings.py View File

@@ -0,0 +1,155 @@
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 learnings api."""
18
+
19
+import ast
20
+import re
21
+import string
22
+
23
+from oslo_log import log
24
+from oslo_utils import strutils
25
+from oslo_utils import uuidutils
26
+import six
27
+import webob
28
+from webob import exc
29
+
30
+from meteos.api import common
31
+from meteos.api.openstack import wsgi
32
+from meteos.api.views import learnings as learning_views
33
+from meteos import exception
34
+from meteos.i18n import _, _LI
35
+from meteos import engine
36
+
37
+LOG = log.getLogger(__name__)
38
+