diff --git a/nimble/api/app.py b/nimble/api/app.py index 7fcb6601..4873131b 100644 --- a/nimble/api/app.py +++ b/nimble/api/app.py @@ -45,3 +45,12 @@ def setup_app(pecan_config=None, extra_hooks=None): ) return app + + +class VersionSelectorApplication(object): + def __init__(self): + pc = get_pecan_config() + self.v1 = setup_app(pecan_config=pc) + + def __call__(self, environ, start_response): + return self.v1(environ, start_response) diff --git a/nimble/cmd/__init__.py b/nimble/cmd/__init__.py new file mode 100644 index 00000000..9b100f75 --- /dev/null +++ b/nimble/cmd/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import oslo_i18n as i18n + +i18n.install('nimble') diff --git a/nimble/cmd/api.py b/nimble/cmd/api.py new file mode 100644 index 00000000..0b54a99d --- /dev/null +++ b/nimble/cmd/api.py @@ -0,0 +1,38 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The Nimble Service API.""" + +import sys + +from oslo_config import cfg + +from nimble.common import service as nimble_service + +CONF = cfg.CONF + + +def main(): + # Parse config file and command line options, then start logging + nimble_service.prepare_service(sys.argv) + + # Build and start the WSGI app + launcher = nimble_service.process_launcher() + server = nimble_service.WSGIService('nimble_api', CONF.api.enable_ssl_api) + launcher.launch_service(server, workers=server.workers) + launcher.wait() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/nimble/common/config.py b/nimble/common/config.py new file mode 100644 index 00000000..a8cbc4b6 --- /dev/null +++ b/nimble/common/config.py @@ -0,0 +1,28 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + +from nimble.common import rpc +from nimble import version + + +def parse_args(argv, default_config_files=None): + rpc.set_defaults(control_exchange='nimble') + cfg.CONF(argv[1:], + project='nimble', + version=version.version_info.release_string(), + default_config_files=default_config_files) + rpc.init(cfg.CONF) diff --git a/nimble/common/context.py b/nimble/common/context.py new file mode 100644 index 00000000..92e8b28a --- /dev/null +++ b/nimble/common/context.py @@ -0,0 +1,99 @@ +# -*- encoding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_context import context + + +class RequestContext(context.RequestContext): + """Extends security contexts from the oslo.context library.""" + + def __init__(self, auth_token=None, domain_id=None, domain_name=None, + user=None, tenant=None, is_admin=False, is_public_api=False, + read_only=False, show_deleted=False, request_id=None, + roles=None, show_password=True, overwrite=True): + """Initialize the RequestContext + + :param auth_token: The authentication token of the current request. + :param domain_id: The ID of the domain. + :param domain_name: The name of the domain. + :param user: The name of the user. + :param tenant: The name of the tenant. + :param is_admin: Indicates if the request context is an administrator + context. + :param is_public_api: Specifies whether the request should be processed + without authentication. + :param request_id: The UUID of the request. + :param roles: List of user's roles if any. + :param show_password: Specifies whether passwords should be masked + before sending back to API call. + :param overwrite: Set to False to ensure that the greenthread local + copy of the index is not overwritten. + """ + super(RequestContext, self).__init__(auth_token=auth_token, + user=user, tenant=tenant, + is_admin=is_admin, + read_only=read_only, + show_deleted=show_deleted, + request_id=request_id, + overwrite=overwrite) + self.is_public_api = is_public_api + self.domain_id = domain_id + self.domain_name = domain_name + self.show_password = show_password + # NOTE(dims): roles was added in context.RequestContext recently. + # we should pass roles in __init__ above instead of setting the + # value here once the minimum version of oslo.context is updated. + self.roles = roles or [] + + def to_dict(self): + return {'auth_token': self.auth_token, + 'user': self.user, + 'tenant': self.tenant, + 'is_admin': self.is_admin, + 'read_only': self.read_only, + 'show_deleted': self.show_deleted, + 'request_id': self.request_id, + 'domain_id': self.domain_id, + 'roles': self.roles, + 'domain_name': self.domain_name, + 'show_password': self.show_password, + 'is_public_api': self.is_public_api} + + @classmethod + def from_dict(cls, values): + values.pop('user', None) + values.pop('tenant', None) + return cls(**values) + + def ensure_thread_contain_context(self): + """Ensure threading contains context + + For async/periodic tasks, the context of local thread is missing. + Set it with request context and this is useful to log the request_id + in log messages. + + """ + if context.get_current(): + return + self.update_store() + + +def get_admin_context(): + """Create an administrator context.""" + + context = RequestContext(None, + tenant=None, + is_admin=True, + overwrite=False) + return context diff --git a/nimble/common/exception.py b/nimble/common/exception.py index 7f0f663a..bd491a8f 100644 --- a/nimble/common/exception.py +++ b/nimble/common/exception.py @@ -114,3 +114,7 @@ class TemporaryFailure(NimbleException): class NotAcceptable(NimbleException): _msg_fmt = _("Request not acceptable.") code = http_client.NOT_ACCEPTABLE + + +class ConfigInvalid(NimbleException): + _msg_fmt = _("Invalid configuration file. %(error_msg)s") diff --git a/nimble/common/rpc.py b/nimble/common/rpc.py new file mode 100644 index 00000000..c72e026f --- /dev/null +++ b/nimble/common/rpc.py @@ -0,0 +1,130 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +import oslo_messaging as messaging + +from nimble.common import context as nimble_context +from nimble.common import exception + + +CONF = cfg.CONF +TRANSPORT = None +NOTIFICATION_TRANSPORT = None +NOTIFIER = None + +ALLOWED_EXMODS = [ + exception.__name__, +] +EXTRA_EXMODS = [] + +TRANSPORT_ALIASES = { + 'nimble.rpc.impl_kombu': 'rabbit', + 'nimble.rpc.impl_qpid': 'qpid', + 'nimble.rpc.impl_zmq': 'zmq', +} + + +def init(conf): + global TRANSPORT, NOTIFICATION_TRANSPORT, NOTIFIER + exmods = get_allowed_exmods() + TRANSPORT = messaging.get_transport(conf, + allowed_remote_exmods=exmods, + aliases=TRANSPORT_ALIASES) + NOTIFICATION_TRANSPORT = messaging.get_notification_transport( + conf, + allowed_remote_exmods=exmods, + aliases=TRANSPORT_ALIASES) + serializer = RequestContextSerializer(messaging.JsonPayloadSerializer()) + NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT, + serializer=serializer) + + +def cleanup(): + global TRANSPORT, NOTIFICATION_TRANSPORT, NOTIFIER + assert TRANSPORT is not None + assert NOTIFICATION_TRANSPORT is not None + assert NOTIFIER is not None + TRANSPORT.cleanup() + NOTIFICATION_TRANSPORT.cleanup() + TRANSPORT = NOTIFICATION_TRANSPORT = NOTIFIER = None + + +def set_defaults(control_exchange): + messaging.set_transport_defaults(control_exchange) + + +def add_extra_exmods(*args): + EXTRA_EXMODS.extend(args) + + +def clear_extra_exmods(): + del EXTRA_EXMODS[:] + + +def get_allowed_exmods(): + return ALLOWED_EXMODS + EXTRA_EXMODS + + +class RequestContextSerializer(messaging.Serializer): + + def __init__(self, base): + self._base = base + + def serialize_entity(self, context, entity): + if not self._base: + return entity + return self._base.serialize_entity(context, entity) + + def deserialize_entity(self, context, entity): + if not self._base: + return entity + return self._base.deserialize_entity(context, entity) + + def serialize_context(self, context): + return context.to_dict() + + def deserialize_context(self, context): + return nimble_context.RequestContext.from_dict(context) + + +def get_transport_url(url_str=None): + return messaging.TransportURL.parse(CONF, url_str, TRANSPORT_ALIASES) + + +def get_client(target, version_cap=None, serializer=None): + assert TRANSPORT is not None + serializer = RequestContextSerializer(serializer) + return messaging.RPCClient(TRANSPORT, + target, + version_cap=version_cap, + serializer=serializer) + + +def get_server(target, endpoints, serializer=None): + assert TRANSPORT is not None + serializer = RequestContextSerializer(serializer) + return messaging.get_rpc_server(TRANSPORT, + target, + endpoints, + executor='eventlet', + serializer=serializer) + + +def get_notifier(service=None, host=None, publisher_id=None): + assert NOTIFIER is not None + if not publisher_id: + publisher_id = "%s.%s" % (service, host or CONF.host) + return NOTIFIER.prepare(publisher_id=publisher_id) diff --git a/nimble/common/service.py b/nimble/common/service.py new file mode 100644 index 00000000..b935294a --- /dev/null +++ b/nimble/common/service.py @@ -0,0 +1,106 @@ +# -*- encoding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_concurrency import processutils +from oslo_log import log +from oslo_service import service +from oslo_service import wsgi + +from nimble.api import app +from nimble.common import config +from nimble.common import exception +from nimble.common.i18n import _ +from nimble.conf import CONF + +LOG = log.getLogger(__name__) + + +def prepare_service(argv=None): + argv = [] if argv is None else argv + log.register_options(CONF) + log.set_defaults(default_log_levels=['amqp=WARNING', + 'amqplib=WARNING', + 'qpid.messaging=INFO', + 'oslo_messaging=INFO', + 'sqlalchemy=WARNING', + 'stevedore=INFO', + 'eventlet.wsgi.server=INFO', + 'iso8601=WARNING', + 'paramiko=WARNING', + 'requests=WARNING', + 'neutronclient=WARNING', + 'glanceclient=WARNING', + 'urllib3.connectionpool=WARNING', + 'keystonemiddleware.auth_token=INFO', + 'keystoneauth.session=INFO', + ]) + config.parse_args(argv) + log.setup(CONF, 'nimble') + + +def process_launcher(): + return service.ProcessLauncher(CONF) + + +class WSGIService(service.ServiceBase): + """Provides ability to launch nimble API from wsgi app.""" + + def __init__(self, name, use_ssl=False): + """Initialize, but do not start the WSGI server. + + :param name: The name of the WSGI server given to the loader. + :param use_ssl: Wraps the socket in an SSL context if True. + :returns: None + """ + self.name = name + self.app = app.VersionSelectorApplication() + self.workers = (CONF.api.api_workers or + processutils.get_worker_count()) + if self.workers and self.workers < 1: + raise exception.ConfigInvalid( + _("api_workers value of %d is invalid, " + "must be greater than 0.") % self.workers) + + self.server = wsgi.Server(CONF, name, self.app, + host=CONF.api.host_ip, + port=CONF.api.port, + use_ssl=use_ssl) + + def start(self): + """Start serving this service using loaded configuration. + + :returns: None + """ + self.server.start() + + def stop(self): + """Stop serving this API. + + :returns: None + """ + self.server.stop() + + def wait(self): + """Wait for the service to stop serving this API. + + :returns: None + """ + self.server.wait() + + def reset(self): + """Reset server greenpool size to default. + + :returns: None + """ + self.server.reset() diff --git a/nimble/conf/__init__.py b/nimble/conf/__init__.py new file mode 100644 index 00000000..0ea01ea2 --- /dev/null +++ b/nimble/conf/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + +from nimble.conf import api + +CONF = cfg.CONF + +api.register_opts(CONF) diff --git a/nimble/conf/api.py b/nimble/conf/api.py new file mode 100644 index 00000000..17f104d2 --- /dev/null +++ b/nimble/conf/api.py @@ -0,0 +1,59 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + +from nimble.common.i18n import _ + +opts = [ + cfg.StrOpt('host_ip', + default='0.0.0.0', + help=_('The IP address on which nimble-api listens.')), + cfg.PortOpt('port', + default=6688, + help=_('The TCP port on which nimble-api listens.')), + cfg.IntOpt('max_limit', + default=1000, + help=_('The maximum number of items returned in a single ' + 'response from a collection resource.')), + cfg.StrOpt('public_endpoint', + help=_("Public URL to use when building the links to the API " + "resources (for example, \"https://nimble.rocks:6688\")." + " If None the links will be built using the request's " + "host URL. If the API is operating behind a proxy, you " + "will want to change this to represent the proxy's URL. " + "Defaults to None.")), + cfg.IntOpt('api_workers', + help=_('Number of workers for OpenStack Nimble API service. ' + 'The default is equal to the number of CPUs available ' + 'if that can be determined, else a default worker ' + 'count of 1 is returned.')), + cfg.BoolOpt('enable_ssl_api', + default=False, + help=_("Enable the integrated stand-alone API to service " + "requests via HTTPS instead of HTTP. If there is a " + "front-end service performing HTTPS offloading from " + "the service, this option should be False; note, you " + "will want to change public API endpoint to represent " + "SSL termination URL with 'public_endpoint' option.")), +] + +opt_group = cfg.OptGroup(name='api', + title='Options for the nimble-api service') + + +def register_opts(conf): + conf.register_group(opt_group) + conf.register_opts(opts, group=opt_group) diff --git a/nimble/version.py b/nimble/version.py new file mode 100644 index 00000000..473d6199 --- /dev/null +++ b/nimble/version.py @@ -0,0 +1,18 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pbr.version + +version_info = pbr.version.VersionInfo('nimble') diff --git a/requirements.txt b/requirements.txt index 59c2f72b..bb49ba09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 +eventlet!=0.18.3,>=0.18.2 # MIT WebOb>=1.2.3 # MIT oslo.concurrency>=3.8.0 # Apache-2.0 oslo.config>=3.14.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 92e33cbc..57f3e134 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,10 @@ classifier = packages = nimble +[entry_points] +console_scripts = + nimble-api = nimble.cmd.api:main + [build_sphinx] source-dir = doc/source build-dir = doc/build @@ -48,4 +52,4 @@ output_file = nimble/locale/nimble.pot [build_releasenotes] all_files = 1 build-dir = releasenotes/build -source-dir = releasenotes/source \ No newline at end of file +source-dir = releasenotes/source