diff --git a/README.rst b/README.rst index 575e9c7..dd3ae14 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,11 @@ now, 8-byte hashes are generated and returned for any ID to report. * GCE allows per-user SSH key specification, but Nova supports only one key. Solution: Nova GCE API just uses first key. +* Default Openstack flavors are available as machine types. GCE doesn't allow symbol '.' in machine type names, +that's why GCE API plugin converts symbols '.' into '-' in 'get' requests (e.g. request of machine types converts +the name 'm1.tiny' into m1-tiny) and vise versa in 'put/post/delete' requests (e.g. instance creation converts +the name 'n1-standard-1' to 'n1.standard.1'). + Authentication specifics ======================== diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 96e7276..58f76aa 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -171,11 +171,12 @@ function configure_gceapi { #------------------------- iniset $GCEAPI_CONF_FILE DEFAULT region $REGION_NAME + iniset $GCEAPI_CONF_FILE DEFAULT keystone_url "$OS_AUTH_URL" - iniset $GCEAPI_CONF_FILE DEFAULT admin_tenant_name $SERVICE_TENANT_NAME - iniset $GCEAPI_CONF_FILE DEFAULT admin_user $GCEAPI_ADMIN_USER - iniset $GCEAPI_CONF_FILE DEFAULT admin_password $SERVICE_PASSWORD - iniset $GCEAPI_CONF_FILE DEFAULT identity_uri "http://${KEYSTONE_AUTH_HOST}:35357/v2.0" + iniset $GCEAPI_CONF_FILE keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME + iniset $GCEAPI_CONF_FILE keystone_authtoken admin_user $GCEAPI_ADMIN_USER + iniset $GCEAPI_CONF_FILE keystone_authtoken admin_password $SERVICE_PASSWORD + iniset $GCEAPI_CONF_FILE keystone_authtoken identity_uri "$OS_AUTH_URL" configure_gceapi_rpc_backend diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..2bf0f0f --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,74 @@ + +from __future__ import print_function + +import sys +import os +import fileinput +import fnmatch + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) +sys.path.insert(0, BASE_DIR) + +# This is required for ReadTheDocs.org, but isn't a bad idea anyway. +os.environ['DJANGO_SETTINGS_MODULE'] = 'openstack_dashboard.settings' + +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', + 'sphinx.ext.viewcode'] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'gce-api' +copyright = '2015, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +#html_theme_path = ["."] +#html_theme = '_theme' +#html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" +html_last_updated_fmt = os.popen(git_cmd).read() + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + '%s Documentation' % project, + 'OpenStack Foundation', 'manual'), +] diff --git a/doc/source/hacking.rst b/doc/source/hacking.rst new file mode 100644 index 0000000..a2bcf4f --- /dev/null +++ b/doc/source/hacking.rst @@ -0,0 +1 @@ +.. include:: ../../HACKING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..ffddbad --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,22 @@ +OpenStack GCE API +===================== + +Support of GCE API for OpenStack. +This project provides a standalone GCE API service that enables +managing of OpenStack Nova service in a manner of Google Cloud Compute +Engine. +It uses port 8787 by default that could be changed via config file.. + +Contents: + +.. toctree:: + :maxdepth: 1 + + hacking + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/gceapi/api/__init__.py b/gceapi/api/__init__.py index 7523c3a..4a2dc29 100644 --- a/gceapi/api/__init__.py +++ b/gceapi/api/__init__.py @@ -40,7 +40,7 @@ gce_opts = [ cfg.StrOpt('network_api', default="neutron", help='Name of network API. neutron(quantum) or nova'), - cfg.StrOpt('keystone_gce_url', + cfg.StrOpt('keystone_url', default='http://127.0.0.1:5000/v2.0', help='Keystone URL'), cfg.StrOpt('public_network', diff --git a/gceapi/api/clients.py b/gceapi/api/clients.py index 6fcd78d..38d930e 100644 --- a/gceapi/api/clients.py +++ b/gceapi/api/clients.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient.v2_0 import client as kc +from keystoneclient import client as kc from novaclient import client as novaclient from novaclient import exceptions as nova_exception from oslo_config import cfg @@ -52,7 +52,7 @@ _nova_api_version = None def nova(context, service_type='compute'): args = { - 'auth_url': CONF.keystone_gce_url, + 'auth_url': CONF.keystone_url, 'auth_token': context.auth_token, 'bypass_url': url_for(context, service_type), } @@ -67,7 +67,7 @@ def neutron(context): return None args = { - 'auth_url': CONF.keystone_gce_url, + 'auth_url': CONF.keystone_url, 'service_type': 'network', 'token': context.auth_token, 'endpoint_url': url_for(context, 'network'), @@ -81,7 +81,7 @@ def glance(context): return None args = { - 'auth_url': CONF.keystone_gce_url, + 'auth_url': CONF.keystone_url, 'service_type': 'image', 'token': context.auth_token, } @@ -96,7 +96,7 @@ def cinder(context): args = { 'service_type': 'volume', - 'auth_url': CONF.keystone_gce_url, + 'auth_url': CONF.keystone_url, 'username': None, 'api_key': None, } @@ -110,18 +110,24 @@ def cinder(context): def keystone(context): - return kc.Client( + c = kc.Client( token=context.auth_token, project_id=context.project_id, tenant_id=context.project_id, - auth_url=CONF.keystone_gce_url) + auth_url=CONF.keystone_url) + if c.auth_ref is None: + # Ver2 doesn't create session and performs + # authentication automatically, but Ver3 does create session + # if it's not provided and doesn't perform authentication. + # TODO(use sessions) + c.authenticate() + return c def url_for(context, service_type): service_catalog = context.service_catalog if not service_catalog: - catalog = keystone(context).service_catalog.catalog - service_catalog = catalog['serviceCatalog'] + service_catalog = keystone(context).service_catalog.get_data() context.service_catalog = service_catalog return get_url_from_catalog(service_catalog, service_type) diff --git a/gceapi/api/discovery.py b/gceapi/api/discovery.py index 989bf4f..c30fcc3 100644 --- a/gceapi/api/discovery.py +++ b/gceapi/api/discovery.py @@ -17,7 +17,7 @@ import os import threading import webob -from keystoneclient.v2_0 import client as keystone_client +from keystoneclient import client as keystone_client from oslo_config import cfg from oslo_log import log as logging @@ -25,7 +25,7 @@ from gceapi.api import clients from gceapi import wsgi_ext as openstack_wsgi LOG = logging.getLogger(__name__) -FLAGS = cfg.CONF +CONF = cfg.CONF class Controller(object): @@ -40,12 +40,20 @@ class Controller(object): if key in self._files: return self._files[key] - tenant = FLAGS.keystone_authtoken["admin_tenant_name"] - user = FLAGS.keystone_authtoken["admin_user"] - password = FLAGS.keystone_authtoken["admin_password"] - keystone = keystone_client.Client(username=user, password=password, - tenant_name=tenant, auth_url=FLAGS.keystone_gce_url) - catalog = keystone.service_catalog.catalog["serviceCatalog"] + auth_data = { + 'project_name': CONF.keystone_authtoken['admin_tenant_name'], + 'username': CONF.keystone_authtoken['admin_user'], + 'password': CONF.keystone_authtoken['admin_password'], + 'auth_url': CONF.keystone_url, + } + keystone = keystone_client.Client(**auth_data) + if keystone.auth_ref is None: + # Ver2 doesn't create session and performs + # authentication automatically, but Ver3 does create session + # if it's not provided and doesn't perform authentication. + # TODO(use sessions) + keystone.authenticate() + catalog = keystone.service_catalog.get_data() public_url = clients.get_url_from_catalog(catalog, "gceapi") if not public_url: public_url = req.host_url @@ -66,7 +74,7 @@ class Controller(object): def _load_file(self, version): file = version + ".json" - protocol_dir = FLAGS.get("protocol_dir") + protocol_dir = CONF.get("protocol_dir") if protocol_dir: file_name = os.path.join(protocol_dir, file) try: diff --git a/gceapi/api/instance_api.py b/gceapi/api/instance_api.py index 187e946..90f1d87 100644 --- a/gceapi/api/instance_api.py +++ b/gceapi/api/instance_api.py @@ -393,7 +393,6 @@ class API(base_api.API): instance = context.operation_data.get("instance") progress = {"progress": int(100.0 * disk_device / full_count)} - disk_device = context.operation_data["disk_device"] disk = context.operation_data.get("disk") if disk: volume_id = disk["id"] @@ -421,8 +420,14 @@ class API(base_api.API): body, scope=scope) disk["id"] = volume["id"] context.operation_data["disk"] = disk - device_name = "vd" + string.ascii_lowercase[disk_device] + # deviceName is optional parameter + # use passed value if given, othewise generate new dev name + device_name = disk.get("deviceName") + if device_name is None: + device_name = "vd" + string.ascii_lowercase[disk_device] + disk["deviceName"] = device_name bdm[device_name] = disk["id"] + if "initializeParams" in disk: return progress disk_device += 1 diff --git a/gceapi/api/oauth.py b/gceapi/api/oauth.py index be11649..e657391 100644 --- a/gceapi/api/oauth.py +++ b/gceapi/api/oauth.py @@ -17,17 +17,16 @@ import json import time import uuid +from keystoneclient import client as keystone_client from keystoneclient import exceptions -from keystoneclient.v2_0 import client as keystone_client from oslo_config import cfg from oslo_log import log as logging -from oslo_utils import timeutils import webob from gceapi.i18n import _ from gceapi import wsgi_ext as openstack_wsgi -FLAGS = cfg.CONF +CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -150,11 +149,16 @@ class Controller(object): keystone = keystone_client.Client( username=username, password=password, - auth_url=FLAGS.keystone_gce_url) - token = keystone.auth_ref["token"] - client.auth_token = token["id"] - s = timeutils.parse_isotime(token["issued_at"]) - e = timeutils.parse_isotime(token["expires"]) + auth_url=CONF.keystone_url) + if keystone.auth_ref is None: + # Ver2 doesn't create session and performs + # authentication automatically, but Ver3 does create session + # if it's not provided and doesn't perform authentication. + # TODO(use sessions) + keystone.authenticate() + client.auth_token = keystone.auth_token + s = keystone.auth_ref.issued + e = keystone.auth_ref.expires client.expires_in = (e - s).seconds except Exception as ex: return webob.exc.HTTPUnauthorized(ex) @@ -201,7 +205,7 @@ class AuthProtocol(object): """Filter for translating oauth token to keystone token.""" def __init__(self, app): self.app = app - self.keystone_url = FLAGS.keystone_gce_url + self.auth_url = CONF.keystone_url def __call__(self, env, start_response): auth_token = env.get("HTTP_AUTHORIZATION") @@ -214,8 +218,15 @@ class AuthProtocol(object): token=auth_token.split()[1], tenant_name=project, force_new_token=True, - auth_url=self.keystone_url) - env["HTTP_X_AUTH_TOKEN"] = keystone.auth_ref["token"]["id"] + auth_url=self.auth_url) + if keystone.auth_ref is None: + # Ver2 doesn't create session and performs + # authentication automatically, but Ver3 does create session + # if it's not provided and doesn't perform authentication. + # TODO(use sessions) + keystone.authenticate() + scoped_token = keystone.auth_token + env["HTTP_X_AUTH_TOKEN"] = scoped_token return self.app(env, start_response) except exceptions.Unauthorized: if project in INTERNAL_GCUTIL_PROJECTS: diff --git a/gceapi/db/sqlalchemy/models.py b/gceapi/db/sqlalchemy/models.py index 57b6de2..c89bb89 100644 --- a/gceapi/db/sqlalchemy/models.py +++ b/gceapi/db/sqlalchemy/models.py @@ -20,6 +20,7 @@ from oslo_db.sqlalchemy import models from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Index, PrimaryKeyConstraint, String, Text + BASE = declarative_base() @@ -34,3 +35,10 @@ class Item(BASE, models.ModelBase): kind = Column(String(length=50)) name = Column(String(length=63)) data = Column(Text()) + + def save(self, session=None): + from gceapi.db.sqlalchemy import api + if session is None: + session = api.get_session() + + super(Item, self).save(session=session) diff --git a/gceapi/tests/contrib/post_test_hook.sh b/gceapi/tests/contrib/post_test_hook.sh index 4e2ddff..0597184 100755 --- a/gceapi/tests/contrib/post_test_hook.sh +++ b/gceapi/tests/contrib/post_test_hook.sh @@ -37,22 +37,27 @@ if [[ ! -f $TEST_CONFIG_DIR/$TEST_CONFIG ]]; then exit 1 fi - # prepare flavors - nova flavor-create --is-public True m1.gceapi 16 512 0 1 - # create separate user/project project_name="project-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 8)" eval $(openstack project create -f shell -c id $project_name) project_id=$id [[ -n "$project_id" ]] || { echo "Can't create project"; exit 1; } user_name="user-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 8)" - password='qwe123QWE' + password='password' eval $(openstack user create "$user_name" --project "$project_id" --password "$password" --email "$user_name@example.com" -f shell -c id) user_id=$id [[ -n "$user_id" ]] || { echo "Can't create user"; exit 1; } # add 'Member' role for swift access role_id=$(openstack role show Member -c id -f value) openstack role add --project $project_id --user $user_id $role_id + + # prepare flavors + flavor_name="n1.standard.1" + if [[ -z "$(nova flavor-list | grep $flavor_name)" ]]; then + nova flavor-create --is-public True $flavor_name 16 512 0 1 + [[ "$?" -eq 0 ]] || { echo "Failed to prepare flavor"; exit 1; } + fi + # create network if [[ -n $(openstack service list | grep neutron) ]]; then net_id=$(neutron net-create --tenant-id $project_id "private" | grep ' id ' | awk '{print $4}') @@ -68,6 +73,19 @@ if [[ ! -f $TEST_CONFIG_DIR/$TEST_CONFIG ]]; then neutron router-gateway-set $router_id $public_net_id [[ "$?" -eq 0 ]] || { echo "router-gateway-set failed"; exit 1; } fi + + #create image in raw format + os_image_name="cirros-0.3.4-raw-image" + if [[ -z "$(openstack image list | grep $os_image_name)" ]]; then + image_name="cirros-0.3.4-x86_64-disk.img" + cirros_image_url="http://download.cirros-cloud.net/0.3.4/$image_name" + sudo rm -f /tmp/$image_name + wget -nv -P /tmp $cirros_image_url + [[ "$?" -eq 0 ]] || { echo "Failed to download image"; exit 1; } + openstack image create --disk-format raw --container-format bare --public --file "/tmp/$image_name" $os_image_name + [[ "$?" -eq 0 ]] || { echo "Failed to prepare image"; exit 1; } + fi + export OS_PROJECT_NAME=$project_name export OS_TENANT_NAME=$project_name export OS_USERNAME=$user_name @@ -76,7 +94,8 @@ if [[ ! -f $TEST_CONFIG_DIR/$TEST_CONFIG ]]; then sudo bash -c "cat > $TEST_CONFIG_DIR/$TEST_CONFIG <