Sync oslo.incubator modules
Update the openstack.common modules from oslo.incubator to have a current copy needed to start using new released oslo libraries. Change-Id: I60ea98b3381cbbc3d2905af224792ee68b423825
This commit is contained in:
parent
5d4d79fbd1
commit
689052700c
@ -322,7 +322,14 @@
|
|||||||
# Options defined in ironic.openstack.common.eventlet_backdoor
|
# Options defined in ironic.openstack.common.eventlet_backdoor
|
||||||
#
|
#
|
||||||
|
|
||||||
# port for eventlet backdoor to listen (integer value)
|
# Enable eventlet backdoor. Acceptable values are 0, <port>,
|
||||||
|
# and <start>:<end>, where 0 results in listening on a random
|
||||||
|
# tcp port number; <port> results in listening on the
|
||||||
|
# specified port number (and not enabling backdoor if that
|
||||||
|
# port is in use); and <start>:<end> results in listening on
|
||||||
|
# the smallest unused port number within the specified range
|
||||||
|
# of port numbers. The chosen port is displayed in the
|
||||||
|
# service's log file. (string value)
|
||||||
#backdoor_port=<None>
|
#backdoor_port=<None>
|
||||||
|
|
||||||
|
|
||||||
@ -330,7 +337,7 @@
|
|||||||
# Options defined in ironic.openstack.common.lockutils
|
# Options defined in ironic.openstack.common.lockutils
|
||||||
#
|
#
|
||||||
|
|
||||||
# Whether to disable inter-process locks. (boolean value)
|
# Enables or disables inter-process locks. (boolean value)
|
||||||
#disable_process_locking=false
|
#disable_process_locking=false
|
||||||
|
|
||||||
# Directory to use for lock files. (string value)
|
# Directory to use for lock files. (string value)
|
||||||
@ -349,47 +356,48 @@
|
|||||||
# of default WARNING level). (boolean value)
|
# of default WARNING level). (boolean value)
|
||||||
#verbose=false
|
#verbose=false
|
||||||
|
|
||||||
# Log output to standard error (boolean value)
|
# Log output to standard error. (boolean value)
|
||||||
#use_stderr=true
|
#use_stderr=true
|
||||||
|
|
||||||
# Format string to use for log messages with context (string
|
# Format string to use for log messages with context. (string
|
||||||
# value)
|
# value)
|
||||||
#logging_context_format_string=%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
|
#logging_context_format_string=%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
|
||||||
|
|
||||||
# Format string to use for log messages without context
|
# Format string to use for log messages without context.
|
||||||
# (string value)
|
# (string value)
|
||||||
#logging_default_format_string=%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
|
#logging_default_format_string=%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
|
||||||
|
|
||||||
# Data to append to log format when level is DEBUG (string
|
# Data to append to log format when level is DEBUG. (string
|
||||||
# value)
|
# value)
|
||||||
#logging_debug_format_suffix=%(funcName)s %(pathname)s:%(lineno)d
|
#logging_debug_format_suffix=%(funcName)s %(pathname)s:%(lineno)d
|
||||||
|
|
||||||
# Prefix each line of exception output with this format
|
# Prefix each line of exception output with this format.
|
||||||
# (string value)
|
# (string value)
|
||||||
#logging_exception_prefix=%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s
|
#logging_exception_prefix=%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s
|
||||||
|
|
||||||
# List of logger=LEVEL pairs (list value)
|
# List of logger=LEVEL pairs. (list value)
|
||||||
#default_log_levels=amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN
|
#default_log_levels=amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN
|
||||||
|
|
||||||
# Publish error events (boolean value)
|
# Enables or disables publication of error events. (boolean
|
||||||
|
# value)
|
||||||
#publish_errors=false
|
#publish_errors=false
|
||||||
|
|
||||||
# Make deprecations fatal (boolean value)
|
# Enables or disables fatal status of deprecations. (boolean
|
||||||
|
# value)
|
||||||
#fatal_deprecations=false
|
#fatal_deprecations=false
|
||||||
|
|
||||||
# If an instance is passed with the log message, format it
|
# The format for an instance that is passed with the log
|
||||||
# like this (string value)
|
# message. (string value)
|
||||||
#instance_format="[instance: %(uuid)s] "
|
#instance_format="[instance: %(uuid)s] "
|
||||||
|
|
||||||
# If an instance UUID is passed with the log message, format
|
# The format for an instance UUID that is passed with the log
|
||||||
# it like this (string value)
|
# message. (string value)
|
||||||
#instance_uuid_format="[instance: %(uuid)s] "
|
#instance_uuid_format="[instance: %(uuid)s] "
|
||||||
|
|
||||||
# The name of logging configuration file. It does not disable
|
# The name of a logging configuration file. This file is
|
||||||
# existing loggers, but just appends specified logging
|
# appended to any existing logging configuration files. For
|
||||||
# configuration to any other existing logging options. Please
|
# details about logging configuration files, see the Python
|
||||||
# see the Python logging module documentation for details on
|
# logging module documentation. (string value)
|
||||||
# logging configuration files. (string value)
|
|
||||||
# Deprecated group/name - [DEFAULT]/log_config
|
# Deprecated group/name - [DEFAULT]/log_config
|
||||||
#log_config_append=<None>
|
#log_config_append=<None>
|
||||||
|
|
||||||
@ -401,7 +409,7 @@
|
|||||||
#log_format=<None>
|
#log_format=<None>
|
||||||
|
|
||||||
# Format string for %%(asctime)s in log records. Default:
|
# Format string for %%(asctime)s in log records. Default:
|
||||||
# %(default)s (string value)
|
# %(default)s . (string value)
|
||||||
#log_date_format=%Y-%m-%d %H:%M:%S
|
#log_date_format=%Y-%m-%d %H:%M:%S
|
||||||
|
|
||||||
# (Optional) Name of log file to output to. If no default is
|
# (Optional) Name of log file to output to. If no default is
|
||||||
@ -410,22 +418,23 @@
|
|||||||
#log_file=<None>
|
#log_file=<None>
|
||||||
|
|
||||||
# (Optional) The base directory used for relative --log-file
|
# (Optional) The base directory used for relative --log-file
|
||||||
# paths (string value)
|
# paths. (string value)
|
||||||
# Deprecated group/name - [DEFAULT]/logdir
|
# Deprecated group/name - [DEFAULT]/logdir
|
||||||
#log_dir=<None>
|
#log_dir=<None>
|
||||||
|
|
||||||
# Use syslog for logging. Existing syslog format is DEPRECATED
|
# Use syslog for logging. Existing syslog format is DEPRECATED
|
||||||
# during I, and then will be changed in J to honor RFC5424
|
# during I, and will change in J to honor RFC5424. (boolean
|
||||||
# (boolean value)
|
# value)
|
||||||
#use_syslog=false
|
#use_syslog=false
|
||||||
|
|
||||||
# (Optional) Use syslog rfc5424 format for logging. If
|
# (Optional) Enables or disables syslog rfc5424 format for
|
||||||
# enabled, will add APP-NAME (RFC5424) before the MSG part of
|
# logging. If enabled, prefixes the MSG part of the syslog
|
||||||
# the syslog message. The old format without APP-NAME is
|
# message with APP-NAME (RFC5424). The format without the APP-
|
||||||
# deprecated in I, and will be removed in J. (boolean value)
|
# NAME is deprecated in I, and will be removed in J. (boolean
|
||||||
|
# value)
|
||||||
#use_syslog_rfc_format=false
|
#use_syslog_rfc_format=false
|
||||||
|
|
||||||
# Syslog facility to receive log lines (string value)
|
# Syslog facility to receive log lines. (string value)
|
||||||
#syslog_log_facility=LOG_USER
|
#syslog_log_facility=LOG_USER
|
||||||
|
|
||||||
|
|
||||||
|
0
ironic/openstack/common/apiclient/__init__.py
Normal file
0
ironic/openstack/common/apiclient/__init__.py
Normal file
221
ironic/openstack/common/apiclient/auth.py
Normal file
221
ironic/openstack/common/apiclient/auth.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# Copyright 2013 Spanish National Research Council.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# E0202: An attribute inherited from %s hide this method
|
||||||
|
# pylint: disable=E0202
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import six
|
||||||
|
from stevedore import extension
|
||||||
|
|
||||||
|
from ironic.openstack.common.apiclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
_discovered_plugins = {}
|
||||||
|
|
||||||
|
|
||||||
|
def discover_auth_systems():
|
||||||
|
"""Discover the available auth-systems.
|
||||||
|
|
||||||
|
This won't take into account the old style auth-systems.
|
||||||
|
"""
|
||||||
|
global _discovered_plugins
|
||||||
|
_discovered_plugins = {}
|
||||||
|
|
||||||
|
def add_plugin(ext):
|
||||||
|
_discovered_plugins[ext.name] = ext.plugin
|
||||||
|
|
||||||
|
ep_namespace = "ironic.openstack.common.apiclient.auth"
|
||||||
|
mgr = extension.ExtensionManager(ep_namespace)
|
||||||
|
mgr.map(add_plugin)
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_system_opts(parser):
|
||||||
|
"""Load options needed by the available auth-systems into a parser.
|
||||||
|
|
||||||
|
This function will try to populate the parser with options from the
|
||||||
|
available plugins.
|
||||||
|
"""
|
||||||
|
group = parser.add_argument_group("Common auth options")
|
||||||
|
BaseAuthPlugin.add_common_opts(group)
|
||||||
|
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
||||||
|
group = parser.add_argument_group(
|
||||||
|
"Auth-system '%s' options" % name,
|
||||||
|
conflict_handler="resolve")
|
||||||
|
auth_plugin.add_opts(group)
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(auth_system):
|
||||||
|
try:
|
||||||
|
plugin_class = _discovered_plugins[auth_system]
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.AuthSystemNotFound(auth_system)
|
||||||
|
return plugin_class(auth_system=auth_system)
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin_from_args(args):
|
||||||
|
"""Load required plugin and populate it with options.
|
||||||
|
|
||||||
|
Try to guess auth system if it is not specified. Systems are tried in
|
||||||
|
alphabetical order.
|
||||||
|
|
||||||
|
:type args: argparse.Namespace
|
||||||
|
:raises: AuthPluginOptionsMissing
|
||||||
|
"""
|
||||||
|
auth_system = args.os_auth_system
|
||||||
|
if auth_system:
|
||||||
|
plugin = load_plugin(auth_system)
|
||||||
|
plugin.parse_opts(args)
|
||||||
|
plugin.sufficient_options()
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
|
||||||
|
plugin_class = _discovered_plugins[plugin_auth_system]
|
||||||
|
plugin = plugin_class()
|
||||||
|
plugin.parse_opts(args)
|
||||||
|
try:
|
||||||
|
plugin.sufficient_options()
|
||||||
|
except exceptions.AuthPluginOptionsMissing:
|
||||||
|
continue
|
||||||
|
return plugin
|
||||||
|
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseAuthPlugin(object):
|
||||||
|
"""Base class for authentication plugins.
|
||||||
|
|
||||||
|
An authentication plugin needs to override at least the authenticate
|
||||||
|
method to be a valid plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
auth_system = None
|
||||||
|
opt_names = []
|
||||||
|
common_opt_names = [
|
||||||
|
"auth_system",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"tenant_name",
|
||||||
|
"token",
|
||||||
|
"auth_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, auth_system=None, **kwargs):
|
||||||
|
self.auth_system = auth_system or self.auth_system
|
||||||
|
self.opts = dict((name, kwargs.get(name))
|
||||||
|
for name in self.opt_names)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parser_add_opt(parser, opt):
|
||||||
|
"""Add an option to parser in two variants.
|
||||||
|
|
||||||
|
:param opt: option name (with underscores)
|
||||||
|
"""
|
||||||
|
dashed_opt = opt.replace("_", "-")
|
||||||
|
env_var = "OS_%s" % opt.upper()
|
||||||
|
arg_default = os.environ.get(env_var, "")
|
||||||
|
arg_help = "Defaults to env[%s]." % env_var
|
||||||
|
parser.add_argument(
|
||||||
|
"--os-%s" % dashed_opt,
|
||||||
|
metavar="<%s>" % dashed_opt,
|
||||||
|
default=arg_default,
|
||||||
|
help=arg_help)
|
||||||
|
parser.add_argument(
|
||||||
|
"--os_%s" % opt,
|
||||||
|
metavar="<%s>" % dashed_opt,
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_opts(cls, parser):
|
||||||
|
"""Populate the parser with the options for this plugin.
|
||||||
|
"""
|
||||||
|
for opt in cls.opt_names:
|
||||||
|
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||||
|
# changed in child classes
|
||||||
|
if opt not in BaseAuthPlugin.common_opt_names:
|
||||||
|
cls._parser_add_opt(parser, opt)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_common_opts(cls, parser):
|
||||||
|
"""Add options that are common for several plugins.
|
||||||
|
"""
|
||||||
|
for opt in cls.common_opt_names:
|
||||||
|
cls._parser_add_opt(parser, opt)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_opt(opt_name, args):
|
||||||
|
"""Return option name and value.
|
||||||
|
|
||||||
|
:param opt_name: name of the option, e.g., "username"
|
||||||
|
:param args: parsed arguments
|
||||||
|
"""
|
||||||
|
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||||
|
|
||||||
|
def parse_opts(self, args):
|
||||||
|
"""Parse the actual auth-system options if any.
|
||||||
|
|
||||||
|
This method is expected to populate the attribute `self.opts` with a
|
||||||
|
dict containing the options and values needed to make authentication.
|
||||||
|
"""
|
||||||
|
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||||
|
for opt_name in self.opt_names))
|
||||||
|
|
||||||
|
def authenticate(self, http_client):
|
||||||
|
"""Authenticate using plugin defined method.
|
||||||
|
|
||||||
|
The method usually analyses `self.opts` and performs
|
||||||
|
a request to authentication server.
|
||||||
|
|
||||||
|
:param http_client: client object that needs authentication
|
||||||
|
:type http_client: HTTPClient
|
||||||
|
:raises: AuthorizationFailure
|
||||||
|
"""
|
||||||
|
self.sufficient_options()
|
||||||
|
self._do_authenticate(http_client)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _do_authenticate(self, http_client):
|
||||||
|
"""Protected method for authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sufficient_options(self):
|
||||||
|
"""Check if all required options are present.
|
||||||
|
|
||||||
|
:raises: AuthPluginOptionsMissing
|
||||||
|
"""
|
||||||
|
missing = [opt
|
||||||
|
for opt in self.opt_names
|
||||||
|
if not self.opts.get(opt)]
|
||||||
|
if missing:
|
||||||
|
raise exceptions.AuthPluginOptionsMissing(missing)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def token_and_endpoint(self, endpoint_type, service_type):
|
||||||
|
"""Return token and endpoint.
|
||||||
|
|
||||||
|
:param service_type: Service type of the endpoint
|
||||||
|
:type service_type: string
|
||||||
|
:param endpoint_type: Type of endpoint.
|
||||||
|
Possible values: public or publicURL,
|
||||||
|
internal or internalURL,
|
||||||
|
admin or adminURL
|
||||||
|
:type endpoint_type: string
|
||||||
|
:returns: tuple of token and endpoint strings
|
||||||
|
:raises: EndpointException
|
||||||
|
"""
|
525
ironic/openstack/common/apiclient/base.py
Normal file
525
ironic/openstack/common/apiclient/base.py
Normal file
@ -0,0 +1,525 @@
|
|||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 OpenStack Foundation
|
||||||
|
# Copyright 2012 Grid Dynamics
|
||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# E1102: %s is not callable
|
||||||
|
# pylint: disable=E1102
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from ironic.openstack.common.apiclient import exceptions
|
||||||
|
from ironic.openstack.common.gettextutils import _
|
||||||
|
from ironic.openstack.common import strutils
|
||||||
|
from ironic.openstack.common import uuidutils
|
||||||
|
|
||||||
|
|
||||||
|
def getid(obj):
|
||||||
|
"""Return id if argument is a Resource.
|
||||||
|
|
||||||
|
Abstracts the common pattern of allowing both an object or an object's ID
|
||||||
|
(UUID) as a parameter when dealing with relationships.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if obj.uuid:
|
||||||
|
return obj.uuid
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return obj.id
|
||||||
|
except AttributeError:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||||
|
class HookableMixin(object):
|
||||||
|
"""Mixin so classes can register and run hooks."""
|
||||||
|
_hooks_map = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_hook(cls, hook_type, hook_func):
|
||||||
|
"""Add a new hook of specified type.
|
||||||
|
|
||||||
|
:param cls: class that registers hooks
|
||||||
|
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||||
|
:param hook_func: hook function
|
||||||
|
"""
|
||||||
|
if hook_type not in cls._hooks_map:
|
||||||
|
cls._hooks_map[hook_type] = []
|
||||||
|
|
||||||
|
cls._hooks_map[hook_type].append(hook_func)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||||
|
"""Run all hooks of specified type.
|
||||||
|
|
||||||
|
:param cls: class that registers hooks
|
||||||
|
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||||
|
:param args: args to be passed to every hook function
|
||||||
|
:param kwargs: kwargs to be passed to every hook function
|
||||||
|
"""
|
||||||
|
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||||
|
for hook_func in hook_funcs:
|
||||||
|
hook_func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseManager(HookableMixin):
|
||||||
|
"""Basic manager type providing common operations.
|
||||||
|
|
||||||
|
Managers interact with a particular type of API (servers, flavors, images,
|
||||||
|
etc.) and provide CRUD operations for them.
|
||||||
|
"""
|
||||||
|
resource_class = None
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
"""Initializes BaseManager with `client`.
|
||||||
|
|
||||||
|
:param client: instance of BaseClient descendant for HTTP requests
|
||||||
|
"""
|
||||||
|
super(BaseManager, self).__init__()
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
def _list(self, url, response_key, obj_class=None, json=None):
|
||||||
|
"""List the collection.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
:param response_key: the key to be looked up in response dictionary,
|
||||||
|
e.g., 'servers'
|
||||||
|
:param obj_class: class for constructing the returned objects
|
||||||
|
(self.resource_class will be used by default)
|
||||||
|
:param json: data that will be encoded as JSON and passed in POST
|
||||||
|
request (GET will be sent by default)
|
||||||
|
"""
|
||||||
|
if json:
|
||||||
|
body = self.client.post(url, json=json).json()
|
||||||
|
else:
|
||||||
|
body = self.client.get(url).json()
|
||||||
|
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
data = body[response_key]
|
||||||
|
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||||
|
# unlike other services which just return the list...
|
||||||
|
try:
|
||||||
|
data = data['values']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||||
|
|
||||||
|
def _get(self, url, response_key):
|
||||||
|
"""Get an object from collection.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
:param response_key: the key to be looked up in response dictionary,
|
||||||
|
e.g., 'server'
|
||||||
|
"""
|
||||||
|
body = self.client.get(url).json()
|
||||||
|
return self.resource_class(self, body[response_key], loaded=True)
|
||||||
|
|
||||||
|
def _head(self, url):
|
||||||
|
"""Retrieve request headers for an object.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
"""
|
||||||
|
resp = self.client.head(url)
|
||||||
|
return resp.status_code == 204
|
||||||
|
|
||||||
|
def _post(self, url, json, response_key, return_raw=False):
|
||||||
|
"""Create an object.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
:param json: data that will be encoded as JSON and passed in POST
|
||||||
|
request (GET will be sent by default)
|
||||||
|
:param response_key: the key to be looked up in response dictionary,
|
||||||
|
e.g., 'servers'
|
||||||
|
:param return_raw: flag to force returning raw JSON instead of
|
||||||
|
Python object of self.resource_class
|
||||||
|
"""
|
||||||
|
body = self.client.post(url, json=json).json()
|
||||||
|
if return_raw:
|
||||||
|
return body[response_key]
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
|
def _put(self, url, json=None, response_key=None):
|
||||||
|
"""Update an object with PUT method.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
:param json: data that will be encoded as JSON and passed in POST
|
||||||
|
request (GET will be sent by default)
|
||||||
|
:param response_key: the key to be looked up in response dictionary,
|
||||||
|
e.g., 'servers'
|
||||||
|
"""
|
||||||
|
resp = self.client.put(url, json=json)
|
||||||
|
# PUT requests may not return a body
|
||||||
|
if resp.content:
|
||||||
|
body = resp.json()
|
||||||
|
if response_key is not None:
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
else:
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _patch(self, url, json=None, response_key=None):
|
||||||
|
"""Update an object with PATCH method.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers'
|
||||||
|
:param json: data that will be encoded as JSON and passed in POST
|
||||||
|
request (GET will be sent by default)
|
||||||
|
:param response_key: the key to be looked up in response dictionary,
|
||||||
|
e.g., 'servers'
|
||||||
|
"""
|
||||||
|
body = self.client.patch(url, json=json).json()
|
||||||
|
if response_key is not None:
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
else:
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _delete(self, url):
|
||||||
|
"""Delete an object.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g., '/servers/my-server'
|
||||||
|
"""
|
||||||
|
return self.client.delete(url)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class ManagerWithFind(BaseManager):
|
||||||
|
"""Manager with additional `find()`/`findall()` methods."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def list(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def find(self, **kwargs):
|
||||||
|
"""Find a single item with attributes matching ``**kwargs``.
|
||||||
|
|
||||||
|
This isn't very efficient: it loads the entire list then filters on
|
||||||
|
the Python side.
|
||||||
|
"""
|
||||||
|
matches = self.findall(**kwargs)
|
||||||
|
num_matches = len(matches)
|
||||||
|
if num_matches == 0:
|
||||||
|
msg = _("No %(name)s matching %(args)s.") % {
|
||||||
|
'name': self.resource_class.__name__,
|
||||||
|
'args': kwargs
|
||||||
|
}
|
||||||
|
raise exceptions.NotFound(msg)
|
||||||
|
elif num_matches > 1:
|
||||||
|
raise exceptions.NoUniqueMatch()
|
||||||
|
else:
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
def findall(self, **kwargs):
|
||||||
|
"""Find all items with attributes matching ``**kwargs``.
|
||||||
|
|
||||||
|
This isn't very efficient: it loads the entire list then filters on
|
||||||
|
the Python side.
|
||||||
|
"""
|
||||||
|
found = []
|
||||||
|
searches = kwargs.items()
|
||||||
|
|
||||||
|
for obj in self.list():
|
||||||
|
try:
|
||||||
|
if all(getattr(obj, attr) == value
|
||||||
|
for (attr, value) in searches):
|
||||||
|
found.append(obj)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
class CrudManager(BaseManager):
|
||||||
|
"""Base manager class for manipulating entities.
|
||||||
|
|
||||||
|
Children of this class are expected to define a `collection_key` and `key`.
|
||||||
|
|
||||||
|
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||||
|
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||||
|
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||||
|
{}, {}]}`).
|
||||||
|
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||||
|
refer to an individual member of the collection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
collection_key = None
|
||||||
|
key = None
|
||||||
|
|
||||||
|
def build_url(self, base_url=None, **kwargs):
|
||||||
|
"""Builds a resource URL for the given kwargs.
|
||||||
|
|
||||||
|
Given an example collection where `collection_key = 'entities'` and
|
||||||
|
`key = 'entity'`, the following URL's could be generated.
|
||||||
|
|
||||||
|
By default, the URL will represent a collection of entities, e.g.::
|
||||||
|
|
||||||
|
/entities
|
||||||
|
|
||||||
|
If kwargs contains an `entity_id`, then the URL will represent a
|
||||||
|
specific member, e.g.::
|
||||||
|
|
||||||
|
/entities/{entity_id}
|
||||||
|
|
||||||
|
:param base_url: if provided, the generated URL will be appended to it
|
||||||
|
"""
|
||||||
|
url = base_url if base_url is not None else ''
|
||||||
|
|
||||||
|
url += '/%s' % self.collection_key
|
||||||
|
|
||||||
|
# do we have a specific entity?
|
||||||
|
entity_id = kwargs.get('%s_id' % self.key)
|
||||||
|
if entity_id is not None:
|
||||||
|
url += '/%s' % entity_id
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
def _filter_kwargs(self, kwargs):
|
||||||
|
"""Drop null values and handle ids."""
|
||||||
|
for key, ref in six.iteritems(kwargs.copy()):
|
||||||
|
if ref is None:
|
||||||
|
kwargs.pop(key)
|
||||||
|
else:
|
||||||
|
if isinstance(ref, Resource):
|
||||||
|
kwargs.pop(key)
|
||||||
|
kwargs['%s_id' % key] = getid(ref)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def create(self, **kwargs):
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
return self._post(
|
||||||
|
self.build_url(**kwargs),
|
||||||
|
{self.key: kwargs},
|
||||||
|
self.key)
|
||||||
|
|
||||||
|
def get(self, **kwargs):
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
return self._get(
|
||||||
|
self.build_url(**kwargs),
|
||||||
|
self.key)
|
||||||
|
|
||||||
|
def head(self, **kwargs):
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
return self._head(self.build_url(**kwargs))
|
||||||
|
|
||||||
|
def list(self, base_url=None, **kwargs):
|
||||||
|
"""List the collection.
|
||||||
|
|
||||||
|
:param base_url: if provided, the generated URL will be appended to it
|
||||||
|
"""
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
|
||||||
|
return self._list(
|
||||||
|
'%(base_url)s%(query)s' % {
|
||||||
|
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||||
|
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||||
|
},
|
||||||
|
self.collection_key)
|
||||||
|
|
||||||
|
def put(self, base_url=None, **kwargs):
|
||||||
|
"""Update an element.
|
||||||
|
|
||||||
|
:param base_url: if provided, the generated URL will be appended to it
|
||||||
|
"""
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
|
||||||
|
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
params = kwargs.copy()
|
||||||
|
params.pop('%s_id' % self.key)
|
||||||
|
|
||||||
|
return self._patch(
|
||||||
|
self.build_url(**kwargs),
|
||||||
|
{self.key: params},
|
||||||
|
self.key)
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
|
||||||
|
return self._delete(
|
||||||
|
self.build_url(**kwargs))
|
||||||
|
|
||||||
|
def find(self, base_url=None, **kwargs):
|
||||||
|
"""Find a single item with attributes matching ``**kwargs``.
|
||||||
|
|
||||||
|
:param base_url: if provided, the generated URL will be appended to it
|
||||||
|
"""
|
||||||
|
kwargs = self._filter_kwargs(kwargs)
|
||||||
|
|
||||||
|
rl = self._list(
|
||||||
|
'%(base_url)s%(query)s' % {
|
||||||
|
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||||
|
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||||
|
},
|
||||||
|
self.collection_key)
|
||||||
|
num = len(rl)
|
||||||
|
|
||||||
|
if num == 0:
|
||||||
|
msg = _("No %(name)s matching %(args)s.") % {
|
||||||
|
'name': self.resource_class.__name__,
|
||||||
|
'args': kwargs
|
||||||
|
}
|
||||||
|
raise exceptions.NotFound(404, msg)
|
||||||
|
elif num > 1:
|
||||||
|
raise exceptions.NoUniqueMatch
|
||||||
|
else:
|
||||||
|
return rl[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Extension(HookableMixin):
|
||||||
|
"""Extension descriptor."""
|
||||||
|
|
||||||
|
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||||
|
manager_class = None
|
||||||
|
|
||||||
|
def __init__(self, name, module):
|
||||||
|
super(Extension, self).__init__()
|
||||||
|
self.name = name
|
||||||
|
self.module = module
|
||||||
|
self._parse_extension_module()
|
||||||
|
|
||||||
|
def _parse_extension_module(self):
|
||||||
|
self.manager_class = None
|
||||||
|
for attr_name, attr_value in self.module.__dict__.items():
|
||||||
|
if attr_name in self.SUPPORTED_HOOKS:
|
||||||
|
self.add_hook(attr_name, attr_value)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if issubclass(attr_value, BaseManager):
|
||||||
|
self.manager_class = attr_value
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Extension '%s'>" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||||
|
|
||||||
|
This is pretty much just a bag for attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
HUMAN_ID = False
|
||||||
|
NAME_ATTR = 'name'
|
||||||
|
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
"""Populate and bind to a manager.
|
||||||
|
|
||||||
|
:param manager: BaseManager object
|
||||||
|
:param info: dictionary representing resource attributes
|
||||||
|
:param loaded: prevent lazy-loading if set to True
|
||||||
|
"""
|
||||||
|
self.manager = manager
|
||||||
|
self._info = info
|
||||||
|
self._add_details(info)
|
||||||
|
self._loaded = loaded
|
||||||
|
self._init_completion_cache()
|
||||||
|
|
||||||
|
def _init_completion_cache(self):
|
||||||
|
cache_write = getattr(self.manager, 'write_to_completion_cache', None)
|
||||||
|
if not cache_write:
|
||||||
|
return
|
||||||
|
|
||||||
|
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
|
||||||
|
# enter an infinite loop of __getattr__ -> get -> __init__ ->
|
||||||
|
# __getattr__ -> ...
|
||||||
|
if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id):
|
||||||
|
cache_write('uuid', self.id)
|
||||||
|
|
||||||
|
if self.human_id:
|
||||||
|
cache_write('human_id', self.human_id)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
reprkeys = sorted(k
|
||||||
|
for k in self.__dict__.keys()
|
||||||
|
if k[0] != '_' and k != 'manager')
|
||||||
|
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||||
|
return "<%s %s>" % (self.__class__.__name__, info)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def human_id(self):
|
||||||
|
"""Human-readable ID which can be used for bash completion.
|
||||||
|
"""
|
||||||
|
if self.HUMAN_ID:
|
||||||
|
name = getattr(self, self.NAME_ATTR, None)
|
||||||
|
if name is not None:
|
||||||
|
return strutils.to_slug(name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _add_details(self, info):
|
||||||
|
for (k, v) in six.iteritems(info):
|
||||||
|
try:
|
||||||
|
setattr(self, k, v)
|
||||||
|
self._info[k] = v
|
||||||
|
except AttributeError:
|
||||||
|
# In this case we already defined the attribute on the class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k not in self.__dict__:
|
||||||
|
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||||
|
if not self.is_loaded():
|
||||||
|
self.get()
|
||||||
|
return self.__getattr__(k)
|
||||||
|
|
||||||
|
raise AttributeError(k)
|
||||||
|
else:
|
||||||
|
return self.__dict__[k]
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Support for lazy loading details.
|
||||||
|
|
||||||
|
Some clients, such as novaclient have the option to lazy load the
|
||||||
|
details, details which can be loaded with this function.
|
||||||
|
"""
|
||||||
|
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||||
|
self.set_loaded(True)
|
||||||
|
if not hasattr(self.manager, 'get'):
|
||||||
|
return
|
||||||
|
|
||||||
|
new = self.manager.get(self.id)
|
||||||
|
if new:
|
||||||
|
self._add_details(new._info)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Resource):
|
||||||
|
return NotImplemented
|
||||||
|
# two resources of different types are not equal
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return False
|
||||||
|
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||||
|
return self.id == other.id
|
||||||
|
return self._info == other._info
|
||||||
|
|
||||||
|
def is_loaded(self):
|
||||||
|
return self._loaded
|
||||||
|
|
||||||
|
def set_loaded(self, val):
|
||||||
|
self._loaded = val
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return copy.deepcopy(self._info)
|
364
ironic/openstack/common/apiclient/client.py
Normal file
364
ironic/openstack/common/apiclient/client.py
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 OpenStack Foundation
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# Copyright 2013 Alessio Ababilov
|
||||||
|
# Copyright 2013 Grid Dynamics
|
||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
OpenStack Client interface. Handles the REST calls and responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# E0202: An attribute inherited from %s hide this method
|
||||||
|
# pylint: disable=E0202
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ironic.openstack.common.apiclient import exceptions
|
||||||
|
from ironic.openstack.common.gettextutils import _
|
||||||
|
from ironic.openstack.common import importutils
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient(object):
|
||||||
|
"""This client handles sending HTTP requests to OpenStack servers.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- share authentication information between several clients to different
|
||||||
|
services (e.g., for compute and image clients);
|
||||||
|
- reissue authentication request for expired tokens;
|
||||||
|
- encode/decode JSON bodies;
|
||||||
|
- raise exceptions on HTTP errors;
|
||||||
|
- pluggable authentication;
|
||||||
|
- store authentication information in a keyring;
|
||||||
|
- store time spent for requests;
|
||||||
|
- register clients for particular services, so one can use
|
||||||
|
`http_client.identity` or `http_client.compute`;
|
||||||
|
- log requests and responses in a format that is easy to copy-and-paste
|
||||||
|
into terminal and send the same request with curl.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_agent = "ironic.openstack.common.apiclient"
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
auth_plugin,
|
||||||
|
region_name=None,
|
||||||
|
endpoint_type="publicURL",
|
||||||
|
original_ip=None,
|
||||||
|
verify=True,
|
||||||
|
cert=None,
|
||||||
|
timeout=None,
|
||||||
|
timings=False,
|
||||||
|
keyring_saver=None,
|
||||||
|
debug=False,
|
||||||
|
user_agent=None,
|
||||||
|
http=None):
|
||||||
|
self.auth_plugin = auth_plugin
|
||||||
|
|
||||||
|
self.endpoint_type = endpoint_type
|
||||||
|
self.region_name = region_name
|
||||||
|
|
||||||
|
self.original_ip = original_ip
|
||||||
|
self.timeout = timeout
|
||||||
|
self.verify = verify
|
||||||
|
self.cert = cert
|
||||||
|
|
||||||
|
self.keyring_saver = keyring_saver
|
||||||
|
self.debug = debug
|
||||||
|
self.user_agent = user_agent or self.user_agent
|
||||||
|
|
||||||
|
self.times = [] # [("item", starttime, endtime), ...]
|
||||||
|
self.timings = timings
|
||||||
|
|
||||||
|
# requests within the same session can reuse TCP connections from pool
|
||||||
|
self.http = http or requests.Session()
|
||||||
|
|
||||||
|
self.cached_token = None
|
||||||
|
|
||||||
|
def _http_log_req(self, method, url, kwargs):
|
||||||
|
if not self.debug:
|
||||||
|
return
|
||||||
|
|
||||||
|
string_parts = [
|
||||||
|
"curl -i",
|
||||||
|
"-X '%s'" % method,
|
||||||
|
"'%s'" % url,
|
||||||
|
]
|
||||||
|
|
||||||
|
for element in kwargs['headers']:
|
||||||
|
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
||||||
|
string_parts.append(header)
|
||||||
|
|
||||||
|
_logger.debug("REQ: %s" % " ".join(string_parts))
|
||||||
|
if 'data' in kwargs:
|
||||||
|
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
||||||
|
|
||||||
|
def _http_log_resp(self, resp):
|
||||||
|
if not self.debug:
|
||||||
|
return
|
||||||
|
_logger.debug(
|
||||||
|
"RESP: [%s] %s\n",
|
||||||
|
resp.status_code,
|
||||||
|
resp.headers)
|
||||||
|
if resp._content_consumed:
|
||||||
|
_logger.debug(
|
||||||
|
"RESP BODY: %s\n",
|
||||||
|
resp.text)
|
||||||
|
|
||||||
|
def serialize(self, kwargs):
|
||||||
|
if kwargs.get('json') is not None:
|
||||||
|
kwargs['headers']['Content-Type'] = 'application/json'
|
||||||
|
kwargs['data'] = json.dumps(kwargs['json'])
|
||||||
|
try:
|
||||||
|
del kwargs['json']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_timings(self):
|
||||||
|
return self.times
|
||||||
|
|
||||||
|
def reset_timings(self):
|
||||||
|
self.times = []
|
||||||
|
|
||||||
|
def request(self, method, url, **kwargs):
|
||||||
|
"""Send an http request with the specified characteristics.
|
||||||
|
|
||||||
|
Wrapper around `requests.Session.request` to handle tasks such as
|
||||||
|
setting headers, JSON encoding/decoding, and error handling.
|
||||||
|
|
||||||
|
:param method: method of HTTP request
|
||||||
|
:param url: URL of HTTP request
|
||||||
|
:param kwargs: any other parameter that can be passed to
|
||||||
|
requests.Session.request (such as `headers`) or `json`
|
||||||
|
that will be encoded as JSON and used as `data` argument
|
||||||
|
"""
|
||||||
|
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
||||||
|
kwargs["headers"]["User-Agent"] = self.user_agent
|
||||||
|
if self.original_ip:
|
||||||
|
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
||||||
|
self.original_ip, self.user_agent)
|
||||||
|
if self.timeout is not None:
|
||||||
|
kwargs.setdefault("timeout", self.timeout)
|
||||||
|
kwargs.setdefault("verify", self.verify)
|
||||||
|
if self.cert is not None:
|
||||||
|
kwargs.setdefault("cert", self.cert)
|
||||||
|
self.serialize(kwargs)
|
||||||
|
|
||||||
|
self._http_log_req(method, url, kwargs)
|
||||||
|
if self.timings:
|
||||||
|
start_time = time.time()
|
||||||
|
resp = self.http.request(method, url, **kwargs)
|
||||||
|
if self.timings:
|
||||||
|
self.times.append(("%s %s" % (method, url),
|
||||||
|
start_time, time.time()))
|
||||||
|
self._http_log_resp(resp)
|
||||||
|
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
_logger.debug(
|
||||||
|
"Request returned failure status: %s",
|
||||||
|
resp.status_code)
|
||||||
|
raise exceptions.from_response(resp, method, url)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def concat_url(endpoint, url):
|
||||||
|
"""Concatenate endpoint and final URL.
|
||||||
|
|
||||||
|
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
||||||
|
"http://keystone/v2.0/tokens".
|
||||||
|
|
||||||
|
:param endpoint: the base URL
|
||||||
|
:param url: the final URL
|
||||||
|
"""
|
||||||
|
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
||||||
|
|
||||||
|
def client_request(self, client, method, url, **kwargs):
|
||||||
|
"""Send an http request using `client`'s endpoint and specified `url`.
|
||||||
|
|
||||||
|
If request was rejected as unauthorized (possibly because the token is
|
||||||
|
expired), issue one authorization attempt and send the request once
|
||||||
|
again.
|
||||||
|
|
||||||
|
:param client: instance of BaseClient descendant
|
||||||
|
:param method: method of HTTP request
|
||||||
|
:param url: URL of HTTP request
|
||||||
|
:param kwargs: any other parameter that can be passed to
|
||||||
|
`HTTPClient.request`
|
||||||
|
"""
|
||||||
|
|
||||||
|
filter_args = {
|
||||||
|
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
||||||
|
"service_type": client.service_type,
|
||||||
|
}
|
||||||
|
token, endpoint = (self.cached_token, client.cached_endpoint)
|
||||||
|
just_authenticated = False
|
||||||
|
if not (token and endpoint):
|
||||||
|
try:
|
||||||
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||||
|
**filter_args)
|
||||||
|
except exceptions.EndpointException:
|
||||||
|
pass
|
||||||
|
if not (token and endpoint):
|
||||||
|
self.authenticate()
|
||||||
|
just_authenticated = True
|
||||||
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||||
|
**filter_args)
|
||||||
|
if not (token and endpoint):
|
||||||
|
raise exceptions.AuthorizationFailure(
|
||||||
|
_("Cannot find endpoint or token for request"))
|
||||||
|
|
||||||
|
old_token_endpoint = (token, endpoint)
|
||||||
|
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
||||||
|
self.cached_token = token
|
||||||
|
client.cached_endpoint = endpoint
|
||||||
|
# Perform the request once. If we get Unauthorized, then it
|
||||||
|
# might be because the auth token expired, so try to
|
||||||
|
# re-authenticate and try again. If it still fails, bail.
|
||||||
|
try:
|
||||||
|
return self.request(
|
||||||
|
method, self.concat_url(endpoint, url), **kwargs)
|
||||||
|
except exceptions.Unauthorized as unauth_ex:
|
||||||
|
if just_authenticated:
|
||||||
|
raise
|
||||||
|
self.cached_token = None
|
||||||
|
client.cached_endpoint = None
|
||||||
|
self.authenticate()
|
||||||
|
try:
|
||||||
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||||
|
**filter_args)
|
||||||
|
except exceptions.EndpointException:
|
||||||
|
raise unauth_ex
|
||||||
|
if (not (token and endpoint) or
|
||||||
|
old_token_endpoint == (token, endpoint)):
|
||||||
|
raise unauth_ex
|
||||||
|
self.cached_token = token
|
||||||
|
client.cached_endpoint = endpoint
|
||||||
|
kwargs["headers"]["X-Auth-Token"] = token
|
||||||
|
return self.request(
|
||||||
|
method, self.concat_url(endpoint, url), **kwargs)
|
||||||
|
|
||||||
|
def add_client(self, base_client_instance):
|
||||||
|
"""Add a new instance of :class:`BaseClient` descendant.
|
||||||
|
|
||||||
|
`self` will store a reference to `base_client_instance`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> def test_clients():
|
||||||
|
... from keystoneclient.auth import keystone
|
||||||
|
... from openstack.common.apiclient import client
|
||||||
|
... auth = keystone.KeystoneAuthPlugin(
|
||||||
|
... username="user", password="pass", tenant_name="tenant",
|
||||||
|
... auth_url="http://auth:5000/v2.0")
|
||||||
|
... openstack_client = client.HTTPClient(auth)
|
||||||
|
... # create nova client
|
||||||
|
... from novaclient.v1_1 import client
|
||||||
|
... client.Client(openstack_client)
|
||||||
|
... # create keystone client
|
||||||
|
... from keystoneclient.v2_0 import client
|
||||||
|
... client.Client(openstack_client)
|
||||||
|
... # use them
|
||||||
|
... openstack_client.identity.tenants.list()
|
||||||
|
... openstack_client.compute.servers.list()
|
||||||
|
"""
|
||||||
|
service_type = base_client_instance.service_type
|
||||||
|
if service_type and not hasattr(self, service_type):
|
||||||
|
setattr(self, service_type, base_client_instance)
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
self.auth_plugin.authenticate(self)
|
||||||
|
# Store the authentication results in the keyring for later requests
|
||||||
|
if self.keyring_saver:
|
||||||
|
self.keyring_saver.save(self)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseClient(object):
|
||||||
|
"""Top-level object to access the OpenStack API.
|
||||||
|
|
||||||
|
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
||||||
|
will handle a bunch of issues such as authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
service_type = None
|
||||||
|
endpoint_type = None # "publicURL" will be used
|
||||||
|
cached_endpoint = None
|
||||||
|
|
||||||
|
def __init__(self, http_client, extensions=None):
|
||||||
|
self.http_client = http_client
|
||||||
|
http_client.add_client(self)
|
||||||
|
|
||||||
|
# Add in any extensions...
|
||||||
|
if extensions:
|
||||||
|
for extension in extensions:
|
||||||
|
if extension.manager_class:
|
||||||
|
setattr(self, extension.name,
|
||||||
|
extension.manager_class(self))
|
||||||
|
|
||||||
|
def client_request(self, method, url, **kwargs):
|
||||||
|
return self.http_client.client_request(
|
||||||
|
self, method, url, **kwargs)
|
||||||
|
|
||||||
|
def head(self, url, **kwargs):
|
||||||
|
return self.client_request("HEAD", url, **kwargs)
|
||||||
|
|
||||||
|
def get(self, url, **kwargs):
|
||||||
|
return self.client_request("GET", url, **kwargs)
|
||||||
|
|
||||||
|
def post(self, url, **kwargs):
|
||||||
|
return self.client_request("POST", url, **kwargs)
|
||||||
|
|
||||||
|
def put(self, url, **kwargs):
|
||||||
|
return self.client_request("PUT", url, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, url, **kwargs):
|
||||||
|
return self.client_request("DELETE", url, **kwargs)
|
||||||
|
|
||||||
|
def patch(self, url, **kwargs):
|
||||||
|
return self.client_request("PATCH", url, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_class(api_name, version, version_map):
|
||||||
|
"""Returns the client class for the requested API version
|
||||||
|
|
||||||
|
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||||
|
:param version: the requested API version
|
||||||
|
:param version_map: a dict of client classes keyed by version
|
||||||
|
:rtype: a client class for the requested API version
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client_path = version_map[str(version)]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
||||||
|
"Must be one of: %(version_map)s") % {
|
||||||
|
'api_name': api_name,
|
||||||
|
'version': version,
|
||||||
|
'version_map': ', '.join(version_map.keys())
|
||||||
|
}
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
return importutils.import_class(client_path)
|
466
ironic/openstack/common/apiclient/exceptions.py
Normal file
466
ironic/openstack/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# Copyright 2013 Alessio Ababilov
|
||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exception definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ironic.openstack.common.gettextutils import _
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""The base exception class for all exceptions this library raises.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArgs(ClientException):
|
||||||
|
"""Supplied arguments are not sufficient for calling a function."""
|
||||||
|
def __init__(self, missing):
|
||||||
|
self.missing = missing
|
||||||
|
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||||
|
super(MissingArgs, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(ClientException):
|
||||||
|
"""Error in validation on API client side."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedVersion(ClientException):
|
||||||
|
"""User is trying to use an unsupported version of the API."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(ClientException):
|
||||||
|
"""Error in CLI tool."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(ClientException):
|
||||||
|
"""Cannot authorize API client."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionRefused(ClientException):
|
||||||
|
"""Cannot connect to API service."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||||
|
"""Auth plugin misses some options."""
|
||||||
|
def __init__(self, opt_names):
|
||||||
|
super(AuthPluginOptionsMissing, self).__init__(
|
||||||
|
_("Authentication failed. Missing options: %s") %
|
||||||
|
", ".join(opt_names))
|
||||||
|
self.opt_names = opt_names
|
||||||
|
|
||||||
|
|
||||||
|
class AuthSystemNotFound(AuthorizationFailure):
|
||||||
|
"""User has specified an AuthSystem that is not installed."""
|
||||||
|
def __init__(self, auth_system):
|
||||||
|
super(AuthSystemNotFound, self).__init__(
|
||||||
|
_("AuthSystemNotFound: %s") % repr(auth_system))
|
||||||
|
self.auth_system = auth_system
|
||||||
|
|
||||||
|
|
||||||
|
class NoUniqueMatch(ClientException):
|
||||||
|
"""Multiple entities found instead of one."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointException(ClientException):
|
||||||
|
"""Something is rotten in Service Catalog."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(EndpointException):
|
||||||
|
"""Could not find requested endpoint in Service Catalog."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AmbiguousEndpoints(EndpointException):
|
||||||
|
"""Found more than one matching endpoint in Service Catalog."""
|
||||||
|
def __init__(self, endpoints=None):
|
||||||
|
super(AmbiguousEndpoints, self).__init__(
|
||||||
|
_("AmbiguousEndpoints: %s") % repr(endpoints))
|
||||||
|
self.endpoints = endpoints
|
||||||
|
|
||||||
|
|
||||||
|
class HttpError(ClientException):
|
||||||
|
"""The base exception class for all HTTP exceptions.
|
||||||
|
"""
|
||||||
|
http_status = 0
|
||||||
|
message = _("HTTP Error")
|
||||||
|
|
||||||
|
def __init__(self, message=None, details=None,
|
||||||
|
response=None, request_id=None,
|
||||||
|
url=None, method=None, http_status=None):
|
||||||
|
self.http_status = http_status or self.http_status
|
||||||
|
self.message = message or self.message
|
||||||
|
self.details = details
|
||||||
|
self.request_id = request_id
|
||||||
|
self.response = response
|
||||||
|
self.url = url
|
||||||
|
self.method = method
|
||||||
|
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||||
|
if request_id:
|
||||||
|
formatted_string += " (Request-ID: %s)" % request_id
|
||||||
|
super(HttpError, self).__init__(formatted_string)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPRedirection(HttpError):
|
||||||
|
"""HTTP Redirection."""
|
||||||
|
message = _("HTTP Redirection")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClientError(HttpError):
|
||||||
|
"""Client-side HTTP error.
|
||||||
|
|
||||||
|
Exception for cases in which the client seems to have erred.
|
||||||
|
"""
|
||||||
|
message = _("HTTP Client Error")
|
||||||
|
|
||||||
|
|
||||||
|
class HttpServerError(HttpError):
|
||||||
|
"""Server-side HTTP error.
|
||||||
|
|
||||||
|
Exception for cases in which the server is aware that it has
|
||||||
|
erred or is incapable of performing the request.
|
||||||
|
"""
|
||||||
|
message = _("HTTP Server Error")
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleChoices(HTTPRedirection):
|
||||||
|
"""HTTP 300 - Multiple Choices.
|
||||||
|
|
||||||
|
Indicates multiple options for the resource that the client may follow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_status = 300
|
||||||
|
message = _("Multiple Choices")
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(HTTPClientError):
|
||||||
|
"""HTTP 400 - Bad Request.
|
||||||
|
|
||||||
|
The request cannot be fulfilled due to bad syntax.
|
||||||
|
"""
|
||||||
|
http_status = 400
|
||||||
|
message = _("Bad Request")
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HTTPClientError):
|
||||||
|
"""HTTP 401 - Unauthorized.
|
||||||
|
|
||||||
|
Similar to 403 Forbidden, but specifically for use when authentication
|
||||||
|
is required and has failed or has not yet been provided.
|
||||||
|
"""
|
||||||
|
http_status = 401
|
||||||
|
message = _("Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentRequired(HTTPClientError):
|
||||||
|
"""HTTP 402 - Payment Required.
|
||||||
|
|
||||||
|
Reserved for future use.
|
||||||
|
"""
|
||||||
|
http_status = 402
|
||||||
|
message = _("Payment Required")
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HTTPClientError):
|
||||||
|
"""HTTP 403 - Forbidden.
|
||||||
|
|
||||||
|
The request was a valid request, but the server is refusing to respond
|
||||||
|
to it.
|
||||||
|
"""
|
||||||
|
http_status = 403
|
||||||
|
message = _("Forbidden")
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPClientError):
|
||||||
|
"""HTTP 404 - Not Found.
|
||||||
|
|
||||||
|
The requested resource could not be found but may be available again
|
||||||
|
in the future.
|
||||||
|
"""
|
||||||
|
http_status = 404
|
||||||
|
message = _("Not Found")
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotAllowed(HTTPClientError):
|
||||||
|
"""HTTP 405 - Method Not Allowed.
|
||||||
|
|
||||||
|
A request was made of a resource using a request method not supported
|
||||||
|
by that resource.
|
||||||
|
"""
|
||||||
|
http_status = 405
|
||||||
|
message = _("Method Not Allowed")
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptable(HTTPClientError):
|
||||||
|
"""HTTP 406 - Not Acceptable.
|
||||||
|
|
||||||
|
The requested resource is only capable of generating content not
|
||||||
|
acceptable according to the Accept headers sent in the request.
|
||||||
|
"""
|
||||||
|
http_status = 406
|
||||||
|
message = _("Not Acceptable")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyAuthenticationRequired(HTTPClientError):
|
||||||
|
"""HTTP 407 - Proxy Authentication Required.
|
||||||
|
|
||||||
|
The client must first authenticate itself with the proxy.
|
||||||
|
"""
|
||||||
|
http_status = 407
|
||||||
|
message = _("Proxy Authentication Required")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTimeout(HTTPClientError):
|
||||||
|
"""HTTP 408 - Request Timeout.
|
||||||
|
|
||||||
|
The server timed out waiting for the request.
|
||||||
|
"""
|
||||||
|
http_status = 408
|
||||||
|
message = _("Request Timeout")
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(HTTPClientError):
|
||||||
|
"""HTTP 409 - Conflict.
|
||||||
|
|
||||||
|
Indicates that the request could not be processed because of conflict
|
||||||
|
in the request, such as an edit conflict.
|
||||||
|
"""
|
||||||
|
http_status = 409
|
||||||
|
message = _("Conflict")
|
||||||
|
|
||||||
|
|
||||||
|
class Gone(HTTPClientError):
|
||||||
|
"""HTTP 410 - Gone.
|
||||||
|
|
||||||
|
Indicates that the resource requested is no longer available and will
|
||||||
|
not be available again.
|
||||||
|
"""
|
||||||
|
http_status = 410
|
||||||
|
message = _("Gone")
|
||||||
|
|
||||||
|
|
||||||
|
class LengthRequired(HTTPClientError):
|
||||||
|
"""HTTP 411 - Length Required.
|
||||||
|
|
||||||
|
The request did not specify the length of its content, which is
|
||||||
|
required by the requested resource.
|
||||||
|
"""
|
||||||
|
http_status = 411
|
||||||
|
message = _("Length Required")
|
||||||
|
|
||||||
|
|
||||||
|
class PreconditionFailed(HTTPClientError):
|
||||||
|
"""HTTP 412 - Precondition Failed.
|
||||||
|
|
||||||
|
The server does not meet one of the preconditions that the requester
|
||||||
|
put on the request.
|
||||||
|
"""
|
||||||
|
http_status = 412
|
||||||
|
message = _("Precondition Failed")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestEntityTooLarge(HTTPClientError):
|
||||||
|
"""HTTP 413 - Request Entity Too Large.
|
||||||
|
|
||||||
|
The request is larger than the server is willing or able to process.
|
||||||
|
"""
|
||||||
|
http_status = 413
|
||||||
|
message = _("Request Entity Too Large")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.retry_after = int(kwargs.pop('retry_after'))
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
self.retry_after = 0
|
||||||
|
|
||||||
|
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestUriTooLong(HTTPClientError):
|
||||||
|
"""HTTP 414 - Request-URI Too Long.
|
||||||
|
|
||||||
|
The URI provided was too long for the server to process.
|
||||||
|
"""
|
||||||
|
http_status = 414
|
||||||
|
message = _("Request-URI Too Long")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedMediaType(HTTPClientError):
|
||||||
|
"""HTTP 415 - Unsupported Media Type.
|
||||||
|
|
||||||
|
The request entity has a media type which the server or resource does
|
||||||
|
not support.
|
||||||
|
"""
|
||||||
|
http_status = 415
|
||||||
|
message = _("Unsupported Media Type")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||||
|
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||||
|
|
||||||
|
The client has asked for a portion of the file, but the server cannot
|
||||||
|
supply that portion.
|
||||||
|
"""
|
||||||
|
http_status = 416
|
||||||
|
message = _("Requested Range Not Satisfiable")
|
||||||
|
|
||||||
|
|
||||||
|
class ExpectationFailed(HTTPClientError):
|
||||||
|
"""HTTP 417 - Expectation Failed.
|
||||||
|
|
||||||
|
The server cannot meet the requirements of the Expect request-header field.
|
||||||
|
"""
|
||||||
|
http_status = 417
|
||||||
|
message = _("Expectation Failed")
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessableEntity(HTTPClientError):
|
||||||
|
"""HTTP 422 - Unprocessable Entity.
|
||||||
|
|
||||||
|
The request was well-formed but was unable to be followed due to semantic
|
||||||
|
errors.
|
||||||
|
"""
|
||||||
|
http_status = 422
|
||||||
|
message = _("Unprocessable Entity")
|
||||||
|
|
||||||
|
|
||||||
|
class InternalServerError(HttpServerError):
|
||||||
|
"""HTTP 500 - Internal Server Error.
|
||||||
|
|
||||||
|
A generic error message, given when no more specific message is suitable.
|
||||||
|
"""
|
||||||
|
http_status = 500
|
||||||
|
message = _("Internal Server Error")
|
||||||
|
|
||||||
|
|
||||||
|
# NotImplemented is a python keyword.
|
||||||
|
class HttpNotImplemented(HttpServerError):
|
||||||
|
"""HTTP 501 - Not Implemented.
|
||||||
|
|
||||||
|
The server either does not recognize the request method, or it lacks
|
||||||
|
the ability to fulfill the request.
|
||||||
|
"""
|
||||||
|
http_status = 501
|
||||||
|
message = _("Not Implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class BadGateway(HttpServerError):
|
||||||
|
"""HTTP 502 - Bad Gateway.
|
||||||
|
|
||||||
|
The server was acting as a gateway or proxy and received an invalid
|
||||||
|
response from the upstream server.
|
||||||
|
"""
|
||||||
|
http_status = 502
|
||||||
|
message = _("Bad Gateway")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HttpServerError):
|
||||||
|
"""HTTP 503 - Service Unavailable.
|
||||||
|
|
||||||
|
The server is currently unavailable.
|
||||||
|
"""
|
||||||
|
http_status = 503
|
||||||
|
message = _("Service Unavailable")
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayTimeout(HttpServerError):
|
||||||
|
"""HTTP 504 - Gateway Timeout.
|
||||||
|
|
||||||
|
The server was acting as a gateway or proxy and did not receive a timely
|
||||||
|
response from the upstream server.
|
||||||
|
"""
|
||||||
|
http_status = 504
|
||||||
|
message = _("Gateway Timeout")
|
||||||
|
|
||||||
|
|
||||||
|
class HttpVersionNotSupported(HttpServerError):
|
||||||
|
"""HTTP 505 - HttpVersion Not Supported.
|
||||||
|
|
||||||
|
The server does not support the HTTP protocol version used in the request.
|
||||||
|
"""
|
||||||
|
http_status = 505
|
||||||
|
message = _("HTTP Version Not Supported")
|
||||||
|
|
||||||
|
|
||||||
|
# _code_map contains all the classes that have http_status attribute.
|
||||||
|
_code_map = dict(
|
||||||
|
(getattr(obj, 'http_status', None), obj)
|
||||||
|
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
||||||
|
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, method, url):
|
||||||
|
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||||
|
|
||||||
|
:param response: instance of `requests.Response` class
|
||||||
|
:param method: HTTP method used for request
|
||||||
|
:param url: URL used for request
|
||||||
|
"""
|
||||||
|
|
||||||
|
req_id = response.headers.get("x-openstack-request-id")
|
||||||
|
# NOTE(hdd) true for older versions of nova and cinder
|
||||||
|
if not req_id:
|
||||||
|
req_id = response.headers.get("x-compute-request-id")
|
||||||
|
kwargs = {
|
||||||
|
"http_status": response.status_code,
|
||||||
|
"response": response,
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"request_id": req_id,
|
||||||
|
}
|
||||||
|
if "retry-after" in response.headers:
|
||||||
|
kwargs["retry_after"] = response.headers["retry-after"]
|
||||||
|
|
||||||
|
content_type = response.headers.get("Content-Type", "")
|
||||||
|
if content_type.startswith("application/json"):
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(body, dict):
|
||||||
|
error = list(body.values())[0]
|
||||||
|
kwargs["message"] = error.get("message")
|
||||||
|
kwargs["details"] = error.get("details")
|
||||||
|
elif content_type.startswith("text/"):
|
||||||
|
kwargs["details"] = response.text
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls = _code_map[response.status_code]
|
||||||
|
except KeyError:
|
||||||
|
if 500 <= response.status_code < 600:
|
||||||
|
cls = HttpServerError
|
||||||
|
elif 400 <= response.status_code < 500:
|
||||||
|
cls = HTTPClientError
|
||||||
|
else:
|
||||||
|
cls = HttpError
|
||||||
|
return cls(**kwargs)
|
173
ironic/openstack/common/apiclient/fake_client.py
Normal file
173
ironic/openstack/common/apiclient/fake_client.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
A fake server that "responds" to API methods with pre-canned responses.
|
||||||
|
|
||||||
|
All of these responses come from the spec, so if for some reason the spec's
|
||||||
|
wrong the tests might raise AssertionError. I've indicated in comments the
|
||||||
|
places where actual behavior differs from the spec.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# W0102: Dangerous default value %s as argument
|
||||||
|
# pylint: disable=W0102
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from ironic.openstack.common.apiclient import client
|
||||||
|
|
||||||
|
|
||||||
|
def assert_has_keys(dct, required=[], optional=[]):
|
||||||
|
for k in required:
|
||||||
|
try:
|
||||||
|
assert k in dct
|
||||||
|
except AssertionError:
|
||||||
|
extra_keys = set(dct.keys()).difference(set(required + optional))
|
||||||
|
raise AssertionError("found unexpected keys: %s" %
|
||||||
|
list(extra_keys))
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponse(requests.Response):
|
||||||
|
"""Wrap requests.Response and provide a convenient initialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
super(TestResponse, self).__init__()
|
||||||
|
self._content_consumed = True
|
||||||
|
if isinstance(data, dict):
|
||||||
|
self.status_code = data.get('status_code', 200)
|
||||||
|
# Fake the text attribute to streamline Response creation
|
||||||
|
text = data.get('text', "")
|
||||||
|
if isinstance(text, (dict, list)):
|
||||||
|
self._content = json.dumps(text)
|
||||||
|
default_headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._content = text
|
||||||
|
default_headers = {}
|
||||||
|
if six.PY3 and isinstance(self._content, six.string_types):
|
||||||
|
self._content = self._content.encode('utf-8', 'strict')
|
||||||
|
self.headers = data.get('headers') or default_headers
|
||||||
|
else:
|
||||||
|
self.status_code = data
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.status_code == other.status_code and
|
||||||
|
self.headers == other.headers and
|
||||||
|
self._content == other._content)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHTTPClient(client.HTTPClient):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.callstack = []
|
||||||
|
self.fixtures = kwargs.pop("fixtures", None) or {}
|
||||||
|
if not args and "auth_plugin" not in kwargs:
|
||||||
|
args = (None, )
|
||||||
|
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def assert_called(self, method, url, body=None, pos=-1):
|
||||||
|
"""Assert than an API method was just called.
|
||||||
|
"""
|
||||||
|
expected = (method, url)
|
||||||
|
called = self.callstack[pos][0:2]
|
||||||
|
assert self.callstack, \
|
||||||
|
"Expected %s %s but no calls were made." % expected
|
||||||
|
|
||||||
|
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||||
|
(expected + called)
|
||||||
|
|
||||||
|
if body is not None:
|
||||||
|
if self.callstack[pos][3] != body:
|
||||||
|
raise AssertionError('%r != %r' %
|
||||||
|
(self.callstack[pos][3], body))
|
||||||
|
|
||||||
|
def assert_called_anytime(self, method, url, body=None):
|
||||||
|
"""Assert than an API method was called anytime in the test.
|
||||||
|
"""
|
||||||
|
expected = (method, url)
|
||||||
|
|
||||||
|
assert self.callstack, \
|
||||||
|
"Expected %s %s but no calls were made." % expected
|
||||||
|
|
||||||
|
found = False
|
||||||
|
entry = None
|
||||||
|
for entry in self.callstack:
|
||||||
|
if expected == entry[0:2]:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
assert found, 'Expected %s %s; got %s' % \
|
||||||
|
(method, url, self.callstack)
|
||||||
|
if body is not None:
|
||||||
|
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
||||||
|
|
||||||
|
self.callstack = []
|
||||||
|
|
||||||
|
def clear_callstack(self):
|
||||||
|
self.callstack = []
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def client_request(self, client, method, url, **kwargs):
|
||||||
|
# Check that certain things are called correctly
|
||||||
|
if method in ["GET", "DELETE"]:
|
||||||
|
assert "json" not in kwargs
|
||||||
|
|
||||||
|
# Note the call
|
||||||
|
self.callstack.append(
|
||||||
|
(method,
|
||||||
|
url,
|
||||||
|
kwargs.get("headers") or {},
|
||||||
|
kwargs.get("json") or kwargs.get("data")))
|
||||||
|
try:
|
||||||
|
fixture = self.fixtures[url][method]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return TestResponse({"headers": fixture[0],
|
||||||
|
"text": fixture[1]})
|
||||||
|
|
||||||
|
# Call the method
|
||||||
|
args = parse.parse_qsl(parse.urlparse(url)[4])
|
||||||
|
kwargs.update(args)
|
||||||
|
munged_url = url.rsplit('?', 1)[0]
|
||||||
|
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||||
|
munged_url = munged_url.replace('-', '_')
|
||||||
|
|
||||||
|
callback = "%s_%s" % (method.lower(), munged_url)
|
||||||
|
|
||||||
|
if not hasattr(self, callback):
|
||||||
|
raise AssertionError('Called unknown API method: %s %s, '
|
||||||
|
'expected fakes method name: %s' %
|
||||||
|
(method, url, callback))
|
||||||
|
|
||||||
|
resp = getattr(self, callback)(**kwargs)
|
||||||
|
if len(resp) == 3:
|
||||||
|
status, headers, body = resp
|
||||||
|
else:
|
||||||
|
status, body = resp
|
||||||
|
headers = {}
|
||||||
|
return TestResponse({
|
||||||
|
"status_code": status,
|
||||||
|
"text": body,
|
||||||
|
"headers": headers,
|
||||||
|
})
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2012 Red Hat, Inc.
|
# Copyright 2012 Red Hat, Inc.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -14,20 +12,26 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
# W0603: Using the global statement
|
||||||
|
# W0621: Redefining name %s from outer scope
|
||||||
|
# pylint: disable=W0603,W0621
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import getpass
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
import prettytable
|
||||||
|
import six
|
||||||
|
from six import moves
|
||||||
|
|
||||||
class MissingArgs(Exception):
|
from ironic.openstack.common.apiclient import exceptions
|
||||||
|
from ironic.openstack.common.gettextutils import _
|
||||||
def __init__(self, missing):
|
from ironic.openstack.common import strutils
|
||||||
self.missing = missing
|
from ironic.openstack.common import uuidutils
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if len(self.missing) == 1:
|
|
||||||
return "An argument is missing"
|
|
||||||
else:
|
|
||||||
return ("%(num)d arguments are missing" %
|
|
||||||
dict(num=len(self.missing)))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_args(fn, *args, **kwargs):
|
def validate_args(fn, *args, **kwargs):
|
||||||
@ -36,11 +40,11 @@ def validate_args(fn, *args, **kwargs):
|
|||||||
>>> validate_args(lambda a: None)
|
>>> validate_args(lambda a: None)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
MissingArgs: An argument is missing
|
MissingArgs: Missing argument(s): a
|
||||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
MissingArgs: 2 arguments are missing
|
MissingArgs: Missing argument(s): b, d
|
||||||
|
|
||||||
:param fn: the function to check
|
:param fn: the function to check
|
||||||
:param arg: the positional arguments supplied
|
:param arg: the positional arguments supplied
|
||||||
@ -52,7 +56,7 @@ def validate_args(fn, *args, **kwargs):
|
|||||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||||
|
|
||||||
def isbound(method):
|
def isbound(method):
|
||||||
return getattr(method, 'im_self', None) is not None
|
return getattr(method, '__self__', None) is not None
|
||||||
|
|
||||||
if isbound(fn):
|
if isbound(fn):
|
||||||
required_args.pop(0)
|
required_args.pop(0)
|
||||||
@ -60,4 +64,254 @@ def validate_args(fn, *args, **kwargs):
|
|||||||
missing = [arg for arg in required_args if arg not in kwargs]
|
missing = [arg for arg in required_args if arg not in kwargs]
|
||||||
missing = missing[len(args):]
|
missing = missing[len(args):]
|
||||||
if missing:
|
if missing:
|
||||||
raise MissingArgs(missing)
|
raise exceptions.MissingArgs(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def arg(*args, **kwargs):
|
||||||
|
"""Decorator for CLI args.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> @arg("name", help="Name of the new entity")
|
||||||
|
... def entity_create(args):
|
||||||
|
... pass
|
||||||
|
"""
|
||||||
|
def _decorator(func):
|
||||||
|
add_arg(func, *args, **kwargs)
|
||||||
|
return func
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
|
def env(*args, **kwargs):
|
||||||
|
"""Returns the first environment variable set.
|
||||||
|
|
||||||
|
If all are empty, defaults to '' or keyword arg `default`.
|
||||||
|
"""
|
||||||
|
for arg in args:
|
||||||
|
value = os.environ.get(arg)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return kwargs.get('default', '')
|
||||||
|
|
||||||
|
|
||||||
|
def add_arg(func, *args, **kwargs):
|
||||||
|
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||||
|
|
||||||
|
if not hasattr(func, 'arguments'):
|
||||||
|
func.arguments = []
|
||||||
|
|
||||||
|
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||||
|
# tests.
|
||||||
|
if (args, kwargs) not in func.arguments:
|
||||||
|
# Because of the semantics of decorator composition if we just append
|
||||||
|
# to the options list positional options will appear to be backwards.
|
||||||
|
func.arguments.insert(0, (args, kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def unauthenticated(func):
|
||||||
|
"""Adds 'unauthenticated' attribute to decorated function.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
>>> @unauthenticated
|
||||||
|
... def mymethod(f):
|
||||||
|
... pass
|
||||||
|
"""
|
||||||
|
func.unauthenticated = True
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def isunauthenticated(func):
|
||||||
|
"""Checks if the function does not require authentication.
|
||||||
|
|
||||||
|
Mark such functions with the `@unauthenticated` decorator.
|
||||||
|
|
||||||
|
:returns: bool
|
||||||
|
"""
|
||||||
|
return getattr(func, 'unauthenticated', False)
|
||||||
|
|
||||||
|
|
||||||
|
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||||
|
mixed_case_fields=None):
|
||||||
|
"""Print a list or objects as a table, one row per object.
|
||||||
|
|
||||||
|
:param objs: iterable of :class:`Resource`
|
||||||
|
:param fields: attributes that correspond to columns, in order
|
||||||
|
:param formatters: `dict` of callables for field formatting
|
||||||
|
:param sortby_index: index of the field for sorting table rows
|
||||||
|
:param mixed_case_fields: fields corresponding to object attributes that
|
||||||
|
have mixed case names (e.g., 'serverId')
|
||||||
|
"""
|
||||||
|
formatters = formatters or {}
|
||||||
|
mixed_case_fields = mixed_case_fields or []
|
||||||
|
if sortby_index is None:
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
kwargs = {'sortby': fields[sortby_index]}
|
||||||
|
pt = prettytable.PrettyTable(fields, caching=False)
|
||||||
|
pt.align = 'l'
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
row = []
|
||||||
|
for field in fields:
|
||||||
|
if field in formatters:
|
||||||
|
row.append(formatters[field](o))
|
||||||
|
else:
|
||||||
|
if field in mixed_case_fields:
|
||||||
|
field_name = field.replace(' ', '_')
|
||||||
|
else:
|
||||||
|
field_name = field.lower().replace(' ', '_')
|
||||||
|
data = getattr(o, field_name, '')
|
||||||
|
row.append(data)
|
||||||
|
pt.add_row(row)
|
||||||
|
|
||||||
|
print(strutils.safe_encode(pt.get_string(**kwargs)))
|
||||||
|
|
||||||
|
|
||||||
|
def print_dict(dct, dict_property="Property", wrap=0):
|
||||||
|
"""Print a `dict` as a table of two columns.
|
||||||
|
|
||||||
|
:param dct: `dict` to print
|
||||||
|
:param dict_property: name of the first column
|
||||||
|
:param wrap: wrapping for the second column
|
||||||
|
"""
|
||||||
|
pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
|
||||||
|
pt.align = 'l'
|
||||||
|
for k, v in six.iteritems(dct):
|
||||||
|
# convert dict to str to check length
|
||||||
|
if isinstance(v, dict):
|
||||||
|
v = six.text_type(v)
|
||||||
|
if wrap > 0:
|
||||||
|
v = textwrap.fill(six.text_type(v), wrap)
|
||||||
|
# if value has a newline, add in multiple rows
|
||||||
|
# e.g. fault with stacktrace
|
||||||
|
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||||
|
lines = v.strip().split(r'\n')
|
||||||
|
col1 = k
|
||||||
|
for line in lines:
|
||||||
|
pt.add_row([col1, line])
|
||||||
|
col1 = ''
|
||||||
|
else:
|
||||||
|
pt.add_row([k, v])
|
||||||
|
print(strutils.safe_encode(pt.get_string()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_password(max_password_prompts=3):
|
||||||
|
"""Read password from TTY."""
|
||||||
|
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||||
|
pw = None
|
||||||
|
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||||
|
# Check for Ctrl-D
|
||||||
|
try:
|
||||||
|
for __ in moves.range(max_password_prompts):
|
||||||
|
pw1 = getpass.getpass("OS Password: ")
|
||||||
|
if verify:
|
||||||
|
pw2 = getpass.getpass("Please verify: ")
|
||||||
|
else:
|
||||||
|
pw2 = pw1
|
||||||
|
if pw1 == pw2 and pw1:
|
||||||
|
pw = pw1
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
return pw
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(manager, name_or_id, **find_args):
|
||||||
|
"""Look for resource in a given manager.
|
||||||
|
|
||||||
|
Used as a helper for the _find_* methods.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _find_hypervisor(cs, hypervisor):
|
||||||
|
#Get a hypervisor by name or ID.
|
||||||
|
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
||||||
|
"""
|
||||||
|
# first try to get entity as integer id
|
||||||
|
try:
|
||||||
|
return manager.get(int(name_or_id))
|
||||||
|
except (TypeError, ValueError, exceptions.NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# now try to get entity as uuid
|
||||||
|
try:
|
||||||
|
if six.PY2:
|
||||||
|
tmp_id = strutils.safe_encode(name_or_id)
|
||||||
|
else:
|
||||||
|
tmp_id = strutils.safe_decode(name_or_id)
|
||||||
|
|
||||||
|
if uuidutils.is_uuid_like(tmp_id):
|
||||||
|
return manager.get(tmp_id)
|
||||||
|
except (TypeError, ValueError, exceptions.NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# for str id which is not uuid
|
||||||
|
if getattr(manager, 'is_alphanum_id_allowed', False):
|
||||||
|
try:
|
||||||
|
return manager.get(name_or_id)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return manager.find(human_id=name_or_id, **find_args)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# finally try to find entity by name
|
||||||
|
try:
|
||||||
|
resource = getattr(manager, 'resource_class', None)
|
||||||
|
name_attr = resource.NAME_ATTR if resource else 'name'
|
||||||
|
kwargs = {name_attr: name_or_id}
|
||||||
|
kwargs.update(find_args)
|
||||||
|
return manager.find(**kwargs)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
msg = _("No %(name)s with a name or "
|
||||||
|
"ID of '%(name_or_id)s' exists.") % \
|
||||||
|
{
|
||||||
|
"name": manager.resource_class.__name__.lower(),
|
||||||
|
"name_or_id": name_or_id
|
||||||
|
}
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
except exceptions.NoUniqueMatch:
|
||||||
|
msg = _("Multiple %(name)s matches found for "
|
||||||
|
"'%(name_or_id)s', use an ID to be more specific.") % \
|
||||||
|
{
|
||||||
|
"name": manager.resource_class.__name__.lower(),
|
||||||
|
"name_or_id": name_or_id
|
||||||
|
}
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def service_type(stype):
|
||||||
|
"""Adds 'service_type' attribute to decorated function.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@service_type('volume')
|
||||||
|
def mymethod(f):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
def inner(f):
|
||||||
|
f.service_type = stype
|
||||||
|
return f
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_type(f):
|
||||||
|
"""Retrieves service type from function."""
|
||||||
|
return getattr(f, 'service_type', None)
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_choice_list(l):
|
||||||
|
return ', '.join("'%s'" % i for i in l)
|
||||||
|
|
||||||
|
|
||||||
|
def exit(msg=''):
|
||||||
|
if msg:
|
||||||
|
print (msg, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
@ -150,7 +150,7 @@ def _import_module(mod_str):
|
|||||||
|
|
||||||
|
|
||||||
def _is_in_group(opt, group):
|
def _is_in_group(opt, group):
|
||||||
"Check if opt is in group."
|
"""Check if opt is in group."""
|
||||||
for value in group._opts.values():
|
for value in group._opts.values():
|
||||||
# NOTE(llu): Temporary workaround for bug #1262148, wait until
|
# NOTE(llu): Temporary workaround for bug #1262148, wait until
|
||||||
# newly released oslo.config support '==' operator.
|
# newly released oslo.config support '==' operator.
|
||||||
@ -159,7 +159,7 @@ def _is_in_group(opt, group):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _guess_groups(opt, mod_obj):
|
def _guess_groups(opt):
|
||||||
# is it in the DEFAULT group?
|
# is it in the DEFAULT group?
|
||||||
if _is_in_group(opt, cfg.CONF):
|
if _is_in_group(opt, cfg.CONF):
|
||||||
return 'DEFAULT'
|
return 'DEFAULT'
|
||||||
@ -193,7 +193,7 @@ def _list_opts(obj):
|
|||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
for opt in opts:
|
for opt in opts:
|
||||||
ret.setdefault(_guess_groups(opt, obj), []).append(opt)
|
ret.setdefault(_guess_groups(opt), []).append(opt)
|
||||||
return ret.items()
|
return ret.items()
|
||||||
|
|
||||||
|
|
||||||
@ -223,6 +223,8 @@ def _get_my_ip():
|
|||||||
|
|
||||||
def _sanitize_default(name, value):
|
def _sanitize_default(name, value):
|
||||||
"""Set up a reasonably sensible default for pybasedir, my_ip and host."""
|
"""Set up a reasonably sensible default for pybasedir, my_ip and host."""
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
fqdn = socket.getfqdn()
|
||||||
if value.startswith(sys.prefix):
|
if value.startswith(sys.prefix):
|
||||||
# NOTE(jd) Don't use os.path.join, because it is likely to think the
|
# NOTE(jd) Don't use os.path.join, because it is likely to think the
|
||||||
# second part is an absolute pathname and therefore drop the first
|
# second part is an absolute pathname and therefore drop the first
|
||||||
@ -234,8 +236,13 @@ def _sanitize_default(name, value):
|
|||||||
return value.replace(BASEDIR, '')
|
return value.replace(BASEDIR, '')
|
||||||
elif value == _get_my_ip():
|
elif value == _get_my_ip():
|
||||||
return '10.0.0.1'
|
return '10.0.0.1'
|
||||||
elif value in (socket.gethostname(), socket.getfqdn()) and 'host' in name:
|
elif value in (hostname, fqdn):
|
||||||
return 'ironic'
|
if 'host' in name:
|
||||||
|
return 'ironic'
|
||||||
|
elif value.endswith(hostname):
|
||||||
|
return value.replace(hostname, 'ironic')
|
||||||
|
elif value.endswith(fqdn):
|
||||||
|
return value.replace(fqdn, 'ironic')
|
||||||
elif value.strip() != value:
|
elif value.strip() != value:
|
||||||
return '"%s"' % value
|
return '"%s"' % value
|
||||||
return value
|
return value
|
||||||
@ -246,7 +253,6 @@ def _print_opt(opt):
|
|||||||
if not opt_help:
|
if not opt_help:
|
||||||
sys.stderr.write('WARNING: "%s" is missing help string.\n' % opt_name)
|
sys.stderr.write('WARNING: "%s" is missing help string.\n' % opt_name)
|
||||||
opt_help = ""
|
opt_help = ""
|
||||||
opt_type = None
|
|
||||||
try:
|
try:
|
||||||
opt_type = OPTION_REGEX.search(str(type(opt))).group(0)
|
opt_type = OPTION_REGEX.search(str(type(opt))).group(0)
|
||||||
except (ValueError, AttributeError) as err:
|
except (ValueError, AttributeError) as err:
|
||||||
|
@ -25,7 +25,7 @@ import uuid
|
|||||||
|
|
||||||
|
|
||||||
def generate_request_id():
|
def generate_request_id():
|
||||||
return 'req-%s' % str(uuid.uuid4())
|
return b'req-' + str(uuid.uuid4()).encode('ascii')
|
||||||
|
|
||||||
|
|
||||||
class RequestContext(object):
|
class RequestContext(object):
|
||||||
@ -77,6 +77,21 @@ class RequestContext(object):
|
|||||||
'instance_uuid': self.instance_uuid,
|
'instance_uuid': self.instance_uuid,
|
||||||
'user_identity': user_idt}
|
'user_identity': user_idt}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, ctx):
|
||||||
|
return cls(
|
||||||
|
auth_token=ctx.get("auth_token"),
|
||||||
|
user=ctx.get("user"),
|
||||||
|
tenant=ctx.get("tenant"),
|
||||||
|
domain=ctx.get("domain"),
|
||||||
|
user_domain=ctx.get("user_domain"),
|
||||||
|
project_domain=ctx.get("project_domain"),
|
||||||
|
is_admin=ctx.get("is_admin", False),
|
||||||
|
read_only=ctx.get("read_only", False),
|
||||||
|
show_deleted=ctx.get("show_deleted", False),
|
||||||
|
request_id=ctx.get("request_id"),
|
||||||
|
instance_uuid=ctx.get("instance_uuid"))
|
||||||
|
|
||||||
|
|
||||||
def get_admin_context(show_deleted=False):
|
def get_admin_context(show_deleted=False):
|
||||||
context = RequestContext(None,
|
context = RequestContext(None,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright (c) 2012 OpenStack Foundation.
|
# Copyright (c) 2012 OpenStack Foundation.
|
||||||
# Administrator of the National Aeronautics and Space Administration.
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
@ -18,8 +16,11 @@
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import errno
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -28,14 +29,33 @@ import eventlet.backdoor
|
|||||||
import greenlet
|
import greenlet
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from ironic.openstack.common.gettextutils import _LI
|
||||||
|
from ironic.openstack.common import log as logging
|
||||||
|
|
||||||
|
help_for_backdoor_port = (
|
||||||
|
"Acceptable values are 0, <port>, and <start>:<end>, where 0 results "
|
||||||
|
"in listening on a random tcp port number; <port> results in listening "
|
||||||
|
"on the specified port number (and not enabling backdoor if that port "
|
||||||
|
"is in use); and <start>:<end> results in listening on the smallest "
|
||||||
|
"unused port number within the specified range of port numbers. The "
|
||||||
|
"chosen port is displayed in the service's log file.")
|
||||||
eventlet_backdoor_opts = [
|
eventlet_backdoor_opts = [
|
||||||
cfg.IntOpt('backdoor_port',
|
cfg.StrOpt('backdoor_port',
|
||||||
default=None,
|
help="Enable eventlet backdoor. %s" % help_for_backdoor_port)
|
||||||
help='port for eventlet backdoor to listen')
|
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(eventlet_backdoor_opts)
|
CONF.register_opts(eventlet_backdoor_opts)
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EventletBackdoorConfigValueError(Exception):
|
||||||
|
def __init__(self, port_range, help_msg, ex):
|
||||||
|
msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. '
|
||||||
|
'%(help)s' %
|
||||||
|
{'range': port_range, 'ex': ex, 'help': help_msg})
|
||||||
|
super(EventletBackdoorConfigValueError, self).__init__(msg)
|
||||||
|
self.port_range = port_range
|
||||||
|
|
||||||
|
|
||||||
def _dont_use_this():
|
def _dont_use_this():
|
||||||
@ -43,7 +63,7 @@ def _dont_use_this():
|
|||||||
|
|
||||||
|
|
||||||
def _find_objects(t):
|
def _find_objects(t):
|
||||||
return filter(lambda o: isinstance(o, t), gc.get_objects())
|
return [o for o in gc.get_objects() if isinstance(o, t)]
|
||||||
|
|
||||||
|
|
||||||
def _print_greenthreads():
|
def _print_greenthreads():
|
||||||
@ -60,6 +80,33 @@ def _print_nativethreads():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_port_range(port_range):
|
||||||
|
if ':' not in port_range:
|
||||||
|
start, end = port_range, port_range
|
||||||
|
else:
|
||||||
|
start, end = port_range.split(':', 1)
|
||||||
|
try:
|
||||||
|
start, end = int(start), int(end)
|
||||||
|
if end < start:
|
||||||
|
raise ValueError
|
||||||
|
return start, end
|
||||||
|
except ValueError as ex:
|
||||||
|
raise EventletBackdoorConfigValueError(port_range, ex,
|
||||||
|
help_for_backdoor_port)
|
||||||
|
|
||||||
|
|
||||||
|
def _listen(host, start_port, end_port, listen_func):
|
||||||
|
try_port = start_port
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return listen_func((host, try_port))
|
||||||
|
except socket.error as exc:
|
||||||
|
if (exc.errno != errno.EADDRINUSE or
|
||||||
|
try_port >= end_port):
|
||||||
|
raise
|
||||||
|
try_port += 1
|
||||||
|
|
||||||
|
|
||||||
def initialize_if_enabled():
|
def initialize_if_enabled():
|
||||||
backdoor_locals = {
|
backdoor_locals = {
|
||||||
'exit': _dont_use_this, # So we don't exit the entire process
|
'exit': _dont_use_this, # So we don't exit the entire process
|
||||||
@ -72,6 +119,8 @@ def initialize_if_enabled():
|
|||||||
if CONF.backdoor_port is None:
|
if CONF.backdoor_port is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
start_port, end_port = _parse_port_range(str(CONF.backdoor_port))
|
||||||
|
|
||||||
# NOTE(johannes): The standard sys.displayhook will print the value of
|
# NOTE(johannes): The standard sys.displayhook will print the value of
|
||||||
# the last expression and set it to __builtin__._, which overwrites
|
# the last expression and set it to __builtin__._, which overwrites
|
||||||
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
||||||
@ -82,8 +131,15 @@ def initialize_if_enabled():
|
|||||||
pprint.pprint(val)
|
pprint.pprint(val)
|
||||||
sys.displayhook = displayhook
|
sys.displayhook = displayhook
|
||||||
|
|
||||||
sock = eventlet.listen(('localhost', CONF.backdoor_port))
|
sock = _listen('localhost', start_port, end_port, eventlet.listen)
|
||||||
|
|
||||||
|
# In the case of backdoor port being zero, a port number is assigned by
|
||||||
|
# listen(). In any case, pull the port number out here.
|
||||||
port = sock.getsockname()[1]
|
port = sock.getsockname()[1]
|
||||||
|
LOG.info(
|
||||||
|
_LI('Eventlet backdoor listening on %(port)s for process %(pid)d') %
|
||||||
|
{'port': port, 'pid': os.getpid()}
|
||||||
|
)
|
||||||
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
||||||
locals=backdoor_locals)
|
locals=backdoor_locals)
|
||||||
return port
|
return port
|
||||||
|
@ -50,8 +50,8 @@ def read_cached_file(filename, force_reload=False):
|
|||||||
"""
|
"""
|
||||||
global _FILE_CACHE
|
global _FILE_CACHE
|
||||||
|
|
||||||
if force_reload and filename in _FILE_CACHE:
|
if force_reload:
|
||||||
del _FILE_CACHE[filename]
|
delete_cached_file(filename)
|
||||||
|
|
||||||
reloaded = False
|
reloaded = False
|
||||||
mtime = os.path.getmtime(filename)
|
mtime = os.path.getmtime(filename)
|
||||||
@ -66,6 +66,17 @@ def read_cached_file(filename, force_reload=False):
|
|||||||
return (reloaded, cache_info['data'])
|
return (reloaded, cache_info['data'])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_cached_file(filename):
|
||||||
|
"""Delete cached file if present.
|
||||||
|
|
||||||
|
:param filename: filename to delete
|
||||||
|
"""
|
||||||
|
global _FILE_CACHE
|
||||||
|
|
||||||
|
if filename in _FILE_CACHE:
|
||||||
|
del _FILE_CACHE[filename]
|
||||||
|
|
||||||
|
|
||||||
def delete_if_exists(path, remove=os.unlink):
|
def delete_if_exists(path, remove=os.unlink):
|
||||||
"""Delete a file, but ignore file not found error.
|
"""Delete a file, but ignore file not found error.
|
||||||
|
|
||||||
@ -99,13 +110,13 @@ def remove_path_on_error(path, remove=delete_if_exists):
|
|||||||
def file_open(*args, **kwargs):
|
def file_open(*args, **kwargs):
|
||||||
"""Open file
|
"""Open file
|
||||||
|
|
||||||
see built-in file() documentation for more details
|
see built-in open() documentation for more details
|
||||||
|
|
||||||
Note: The reason this is kept in a separate module is to easily
|
Note: The reason this is kept in a separate module is to easily
|
||||||
be able to provide a stub module that doesn't alter system
|
be able to provide a stub module that doesn't alter system
|
||||||
state at all (for unit tests)
|
state at all (for unit tests)
|
||||||
"""
|
"""
|
||||||
return file(*args, **kwargs)
|
return open(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def write_to_tempfile(content, path=None, suffix='', prefix='tmp'):
|
def write_to_tempfile(content, path=None, suffix='', prefix='tmp'):
|
||||||
|
@ -23,34 +23,123 @@ Usual usage in an openstack.common module:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import functools
|
|
||||||
import gettext
|
import gettext
|
||||||
import locale
|
import locale
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
from babel import localedata
|
from babel import localedata
|
||||||
import six
|
import six
|
||||||
|
|
||||||
_localedir = os.environ.get('ironic'.upper() + '_LOCALEDIR')
|
|
||||||
_t = gettext.translation('ironic', localedir=_localedir, fallback=True)
|
|
||||||
|
|
||||||
# We use separate translation catalogs for each log level, so set up a
|
|
||||||
# mapping between the log level name and the translator. The domain
|
|
||||||
# for the log level is project_name + "-log-" + log_level so messages
|
|
||||||
# for each level end up in their own catalog.
|
|
||||||
_t_log_levels = dict(
|
|
||||||
(level, gettext.translation('ironic' + '-log-' + level,
|
|
||||||
localedir=_localedir,
|
|
||||||
fallback=True))
|
|
||||||
for level in ['info', 'warning', 'error', 'critical']
|
|
||||||
)
|
|
||||||
|
|
||||||
_AVAILABLE_LANGUAGES = {}
|
_AVAILABLE_LANGUAGES = {}
|
||||||
|
|
||||||
|
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
|
||||||
USE_LAZY = False
|
USE_LAZY = False
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatorFactory(object):
|
||||||
|
"""Create translator functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, domain, localedir=None):
|
||||||
|
"""Establish a set of translation functions for the domain.
|
||||||
|
|
||||||
|
:param domain: Name of translation domain,
|
||||||
|
specifying a message catalog.
|
||||||
|
:type domain: str
|
||||||
|
:param lazy: Delays translation until a message is emitted.
|
||||||
|
Defaults to False.
|
||||||
|
:type lazy: Boolean
|
||||||
|
:param localedir: Directory with translation catalogs.
|
||||||
|
:type localedir: str
|
||||||
|
"""
|
||||||
|
self.domain = domain
|
||||||
|
if localedir is None:
|
||||||
|
localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
|
||||||
|
self.localedir = localedir
|
||||||
|
|
||||||
|
def _make_translation_func(self, domain=None):
|
||||||
|
"""Return a new translation function ready for use.
|
||||||
|
|
||||||
|
Takes into account whether or not lazy translation is being
|
||||||
|
done.
|
||||||
|
|
||||||
|
The domain can be specified to override the default from the
|
||||||
|
factory, but the localedir from the factory is always used
|
||||||
|
because we assume the log-level translation catalogs are
|
||||||
|
installed in the same directory as the main application
|
||||||
|
catalog.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if domain is None:
|
||||||
|
domain = self.domain
|
||||||
|
t = gettext.translation(domain,
|
||||||
|
localedir=self.localedir,
|
||||||
|
fallback=True)
|
||||||
|
# Use the appropriate method of the translation object based
|
||||||
|
# on the python version.
|
||||||
|
m = t.gettext if six.PY3 else t.ugettext
|
||||||
|
|
||||||
|
def f(msg):
|
||||||
|
"""oslo.i18n.gettextutils translation function."""
|
||||||
|
if USE_LAZY:
|
||||||
|
return Message(msg, domain=domain)
|
||||||
|
return m(msg)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary(self):
|
||||||
|
"The default translation function."
|
||||||
|
return self._make_translation_func()
|
||||||
|
|
||||||
|
def _make_log_translation_func(self, level):
|
||||||
|
return self._make_translation_func(self.domain + '-log-' + level)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_info(self):
|
||||||
|
"Translate info-level log messages."
|
||||||
|
return self._make_log_translation_func('info')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_warning(self):
|
||||||
|
"Translate warning-level log messages."
|
||||||
|
return self._make_log_translation_func('warning')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_error(self):
|
||||||
|
"Translate error-level log messages."
|
||||||
|
return self._make_log_translation_func('error')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_critical(self):
|
||||||
|
"Translate critical-level log messages."
|
||||||
|
return self._make_log_translation_func('critical')
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(dhellmann): When this module moves out of the incubator into
|
||||||
|
# oslo.i18n, these global variables can be moved to an integration
|
||||||
|
# module within each application.
|
||||||
|
|
||||||
|
# Create the global translation functions.
|
||||||
|
_translators = TranslatorFactory('ironic')
|
||||||
|
|
||||||
|
# The primary translation function using the well-known name "_"
|
||||||
|
_ = _translators.primary
|
||||||
|
|
||||||
|
# Translators for log levels.
|
||||||
|
#
|
||||||
|
# The abbreviated names are meant to reflect the usual use of a short
|
||||||
|
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||||
|
# the level.
|
||||||
|
_LI = _translators.log_info
|
||||||
|
_LW = _translators.log_warning
|
||||||
|
_LE = _translators.log_error
|
||||||
|
_LC = _translators.log_critical
|
||||||
|
|
||||||
|
# NOTE(dhellmann): End of globals that will move to the application's
|
||||||
|
# integration module.
|
||||||
|
|
||||||
|
|
||||||
def enable_lazy():
|
def enable_lazy():
|
||||||
"""Convenience function for configuring _() to use lazy gettext
|
"""Convenience function for configuring _() to use lazy gettext
|
||||||
|
|
||||||
@ -63,38 +152,7 @@ def enable_lazy():
|
|||||||
USE_LAZY = True
|
USE_LAZY = True
|
||||||
|
|
||||||
|
|
||||||
def _(msg):
|
def install(domain):
|
||||||
if USE_LAZY:
|
|
||||||
return Message(msg, domain='ironic')
|
|
||||||
else:
|
|
||||||
if six.PY3:
|
|
||||||
return _t.gettext(msg)
|
|
||||||
return _t.ugettext(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def _log_translation(msg, level):
|
|
||||||
"""Build a single translation of a log message
|
|
||||||
"""
|
|
||||||
if USE_LAZY:
|
|
||||||
return Message(msg, domain='ironic' + '-log-' + level)
|
|
||||||
else:
|
|
||||||
translator = _t_log_levels[level]
|
|
||||||
if six.PY3:
|
|
||||||
return translator.gettext(msg)
|
|
||||||
return translator.ugettext(msg)
|
|
||||||
|
|
||||||
# Translators for log levels.
|
|
||||||
#
|
|
||||||
# The abbreviated names are meant to reflect the usual use of a short
|
|
||||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
|
||||||
# the level.
|
|
||||||
_LI = functools.partial(_log_translation, level='info')
|
|
||||||
_LW = functools.partial(_log_translation, level='warning')
|
|
||||||
_LE = functools.partial(_log_translation, level='error')
|
|
||||||
_LC = functools.partial(_log_translation, level='critical')
|
|
||||||
|
|
||||||
|
|
||||||
def install(domain, lazy=False):
|
|
||||||
"""Install a _() function using the given translation domain.
|
"""Install a _() function using the given translation domain.
|
||||||
|
|
||||||
Given a translation domain, install a _() function using gettext's
|
Given a translation domain, install a _() function using gettext's
|
||||||
@ -105,43 +163,14 @@ def install(domain, lazy=False):
|
|||||||
a translation-domain-specific environment variable (e.g.
|
a translation-domain-specific environment variable (e.g.
|
||||||
NOVA_LOCALEDIR).
|
NOVA_LOCALEDIR).
|
||||||
|
|
||||||
|
Note that to enable lazy translation, enable_lazy must be
|
||||||
|
called.
|
||||||
|
|
||||||
:param domain: the translation domain
|
:param domain: the translation domain
|
||||||
:param lazy: indicates whether or not to install the lazy _() function.
|
|
||||||
The lazy _() introduces a way to do deferred translation
|
|
||||||
of messages by installing a _ that builds Message objects,
|
|
||||||
instead of strings, which can then be lazily translated into
|
|
||||||
any available locale.
|
|
||||||
"""
|
"""
|
||||||
if lazy:
|
from six import moves
|
||||||
# NOTE(mrodden): Lazy gettext functionality.
|
tf = TranslatorFactory(domain)
|
||||||
#
|
moves.builtins.__dict__['_'] = tf.primary
|
||||||
# The following introduces a deferred way to do translations on
|
|
||||||
# messages in OpenStack. We override the standard _() function
|
|
||||||
# and % (format string) operation to build Message objects that can
|
|
||||||
# later be translated when we have more information.
|
|
||||||
def _lazy_gettext(msg):
|
|
||||||
"""Create and return a Message object.
|
|
||||||
|
|
||||||
Lazy gettext function for a given domain, it is a factory method
|
|
||||||
for a project/module to get a lazy gettext function for its own
|
|
||||||
translation domain (i.e. nova, glance, cinder, etc.)
|
|
||||||
|
|
||||||
Message encapsulates a string so that we can translate
|
|
||||||
it later when needed.
|
|
||||||
"""
|
|
||||||
return Message(msg, domain=domain)
|
|
||||||
|
|
||||||
from six import moves
|
|
||||||
moves.builtins.__dict__['_'] = _lazy_gettext
|
|
||||||
else:
|
|
||||||
localedir = '%s_LOCALEDIR' % domain.upper()
|
|
||||||
if six.PY3:
|
|
||||||
gettext.install(domain,
|
|
||||||
localedir=os.environ.get(localedir))
|
|
||||||
else:
|
|
||||||
gettext.install(domain,
|
|
||||||
localedir=os.environ.get(localedir),
|
|
||||||
unicode=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Message(six.text_type):
|
class Message(six.text_type):
|
||||||
@ -248,47 +277,22 @@ class Message(six.text_type):
|
|||||||
if other is None:
|
if other is None:
|
||||||
params = (other,)
|
params = (other,)
|
||||||
elif isinstance(other, dict):
|
elif isinstance(other, dict):
|
||||||
params = self._trim_dictionary_parameters(other)
|
# Merge the dictionaries
|
||||||
|
# Copy each item in case one does not support deep copy.
|
||||||
|
params = {}
|
||||||
|
if isinstance(self.params, dict):
|
||||||
|
for key, val in self.params.items():
|
||||||
|
params[key] = self._copy_param(val)
|
||||||
|
for key, val in other.items():
|
||||||
|
params[key] = self._copy_param(val)
|
||||||
else:
|
else:
|
||||||
params = self._copy_param(other)
|
params = self._copy_param(other)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def _trim_dictionary_parameters(self, dict_param):
|
|
||||||
"""Return a dict that only has matching entries in the msgid."""
|
|
||||||
# NOTE(luisg): Here we trim down the dictionary passed as parameters
|
|
||||||
# to avoid carrying a lot of unnecessary weight around in the message
|
|
||||||
# object, for example if someone passes in Message() % locals() but
|
|
||||||
# only some params are used, and additionally we prevent errors for
|
|
||||||
# non-deepcopyable objects by unicoding() them.
|
|
||||||
|
|
||||||
# Look for %(param) keys in msgid;
|
|
||||||
# Skip %% and deal with the case where % is first character on the line
|
|
||||||
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
|
|
||||||
|
|
||||||
# If we don't find any %(param) keys but have a %s
|
|
||||||
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
|
|
||||||
# Apparently the full dictionary is the parameter
|
|
||||||
params = self._copy_param(dict_param)
|
|
||||||
else:
|
|
||||||
params = {}
|
|
||||||
# Save our existing parameters as defaults to protect
|
|
||||||
# ourselves from losing values if we are called through an
|
|
||||||
# (erroneous) chain that builds a valid Message with
|
|
||||||
# arguments, and then does something like "msg % kwds"
|
|
||||||
# where kwds is an empty dictionary.
|
|
||||||
src = {}
|
|
||||||
if isinstance(self.params, dict):
|
|
||||||
src.update(self.params)
|
|
||||||
src.update(dict_param)
|
|
||||||
for key in keys:
|
|
||||||
params[key] = self._copy_param(src[key])
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
def _copy_param(self, param):
|
def _copy_param(self, param):
|
||||||
try:
|
try:
|
||||||
return copy.deepcopy(param)
|
return copy.deepcopy(param)
|
||||||
except TypeError:
|
except Exception:
|
||||||
# Fallback to casting to unicode this will handle the
|
# Fallback to casting to unicode this will handle the
|
||||||
# python code-like objects that can't be deep-copied
|
# python code-like objects that can't be deep-copied
|
||||||
return six.text_type(param)
|
return six.text_type(param)
|
||||||
@ -300,13 +304,14 @@ class Message(six.text_type):
|
|||||||
def __radd__(self, other):
|
def __radd__(self, other):
|
||||||
return self.__add__(other)
|
return self.__add__(other)
|
||||||
|
|
||||||
def __str__(self):
|
if six.PY2:
|
||||||
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
|
def __str__(self):
|
||||||
# and it expects specifically a UnicodeError in order to proceed.
|
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
|
||||||
msg = _('Message objects do not support str() because they may '
|
# and it expects specifically a UnicodeError in order to proceed.
|
||||||
'contain non-ascii characters. '
|
msg = _('Message objects do not support str() because they may '
|
||||||
'Please use unicode() or translate() instead.')
|
'contain non-ascii characters. '
|
||||||
raise UnicodeError(msg)
|
'Please use unicode() or translate() instead.')
|
||||||
|
raise UnicodeError(msg)
|
||||||
|
|
||||||
|
|
||||||
def get_available_languages(domain):
|
def get_available_languages(domain):
|
||||||
@ -349,8 +354,8 @@ def get_available_languages(domain):
|
|||||||
'zh_Hant_HK': 'zh_HK',
|
'zh_Hant_HK': 'zh_HK',
|
||||||
'zh_Hant': 'zh_TW',
|
'zh_Hant': 'zh_TW',
|
||||||
'fil': 'tl_PH'}
|
'fil': 'tl_PH'}
|
||||||
for (locale, alias) in six.iteritems(aliases):
|
for (locale_, alias) in six.iteritems(aliases):
|
||||||
if locale in language_list and alias not in language_list:
|
if locale_ in language_list and alias not in language_list:
|
||||||
language_list.append(alias)
|
language_list.append(alias)
|
||||||
|
|
||||||
_AVAILABLE_LANGUAGES[domain] = language_list
|
_AVAILABLE_LANGUAGES[domain] = language_list
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2011 OpenStack Foundation.
|
# Copyright 2011 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -26,10 +24,10 @@ import traceback
|
|||||||
def import_class(import_str):
|
def import_class(import_str):
|
||||||
"""Returns a class from a string including module and class."""
|
"""Returns a class from a string including module and class."""
|
||||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
|
__import__(mod_str)
|
||||||
try:
|
try:
|
||||||
__import__(mod_str)
|
|
||||||
return getattr(sys.modules[mod_str], class_str)
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
except (ValueError, AttributeError):
|
except AttributeError:
|
||||||
raise ImportError('Class %s cannot be found (%s)' %
|
raise ImportError('Class %s cannot be found (%s)' %
|
||||||
(class_str,
|
(class_str,
|
||||||
traceback.format_exception(*sys.exc_info())))
|
traceback.format_exception(*sys.exc_info())))
|
||||||
@ -60,6 +58,13 @@ def import_module(import_str):
|
|||||||
return sys.modules[import_str]
|
return sys.modules[import_str]
|
||||||
|
|
||||||
|
|
||||||
|
def import_versioned_module(version, submodule=None):
|
||||||
|
module = 'ironic.v%s' % version
|
||||||
|
if submodule:
|
||||||
|
module = '.'.join((module, submodule))
|
||||||
|
return import_module(module)
|
||||||
|
|
||||||
|
|
||||||
def try_import(import_str, default=None):
|
def try_import(import_str, default=None):
|
||||||
"""Try to import a module and if it fails return default."""
|
"""Try to import a module and if it fails return default."""
|
||||||
try:
|
try:
|
||||||
|
@ -168,12 +168,16 @@ def dumps(value, default=to_primitive, **kwargs):
|
|||||||
return json.dumps(value, default=default, **kwargs)
|
return json.dumps(value, default=default, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def loads(s, encoding='utf-8'):
|
def dump(obj, fp, *args, **kwargs):
|
||||||
return json.loads(strutils.safe_decode(s, encoding))
|
return json.dump(obj, fp, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def load(fp, encoding='utf-8'):
|
def loads(s, encoding='utf-8', **kwargs):
|
||||||
return json.load(codecs.getreader(encoding)(fp))
|
return json.loads(strutils.safe_decode(s, encoding), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def load(fp, encoding='utf-8', **kwargs):
|
||||||
|
return json.load(codecs.getreader(encoding)(fp), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2011 OpenStack Foundation.
|
# Copyright 2011 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2011 OpenStack Foundation.
|
# Copyright 2011 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -15,10 +13,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -31,9 +29,7 @@ import weakref
|
|||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
from ironic.openstack.common import fileutils
|
from ironic.openstack.common import fileutils
|
||||||
from ironic.openstack.common.gettextutils import _ # noqa
|
from ironic.openstack.common.gettextutils import _, _LE, _LI
|
||||||
from ironic.openstack.common import local
|
|
||||||
from ironic.openstack.common import log as logging
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -41,10 +37,10 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
util_opts = [
|
util_opts = [
|
||||||
cfg.BoolOpt('disable_process_locking', default=False,
|
cfg.BoolOpt('disable_process_locking', default=False,
|
||||||
help='Whether to disable inter-process locks.'),
|
help='Enables or disables inter-process locks.'),
|
||||||
cfg.StrOpt('lock_path',
|
cfg.StrOpt('lock_path',
|
||||||
default=os.environ.get("IRONIC_LOCK_PATH"),
|
default=os.environ.get("IRONIC_LOCK_PATH"),
|
||||||
help=('Directory to use for lock files.'))
|
help='Directory to use for lock files.')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -56,7 +52,7 @@ def set_defaults(lock_path):
|
|||||||
cfg.set_defaults(util_opts, lock_path=lock_path)
|
cfg.set_defaults(util_opts, lock_path=lock_path)
|
||||||
|
|
||||||
|
|
||||||
class _InterProcessLock(object):
|
class _FileLock(object):
|
||||||
"""Lock implementation which allows multiple locks, working around
|
"""Lock implementation which allows multiple locks, working around
|
||||||
issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
|
issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
|
||||||
not require any cleanup. Since the lock is always held on a file
|
not require any cleanup. Since the lock is always held on a file
|
||||||
@ -78,7 +74,13 @@ class _InterProcessLock(object):
|
|||||||
self.lockfile = None
|
self.lockfile = None
|
||||||
self.fname = name
|
self.fname = name
|
||||||
|
|
||||||
def __enter__(self):
|
def acquire(self):
|
||||||
|
basedir = os.path.dirname(self.fname)
|
||||||
|
|
||||||
|
if not os.path.exists(basedir):
|
||||||
|
fileutils.ensure_tree(basedir)
|
||||||
|
LOG.info(_LI('Created lock path: %s'), basedir)
|
||||||
|
|
||||||
self.lockfile = open(self.fname, 'w')
|
self.lockfile = open(self.fname, 'w')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -88,23 +90,41 @@ class _InterProcessLock(object):
|
|||||||
# Also upon reading the MSDN docs for locking(), it seems
|
# Also upon reading the MSDN docs for locking(), it seems
|
||||||
# to have a laughable 10 attempts "blocking" mechanism.
|
# to have a laughable 10 attempts "blocking" mechanism.
|
||||||
self.trylock()
|
self.trylock()
|
||||||
return self
|
LOG.debug('Got file lock "%s"', self.fname)
|
||||||
|
return True
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
if e.errno in (errno.EACCES, errno.EAGAIN):
|
if e.errno in (errno.EACCES, errno.EAGAIN):
|
||||||
# external locks synchronise things like iptables
|
# external locks synchronise things like iptables
|
||||||
# updates - give it some time to prevent busy spinning
|
# updates - give it some time to prevent busy spinning
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
else:
|
else:
|
||||||
raise
|
raise threading.ThreadError(_("Unable to acquire lock on"
|
||||||
|
" `%(filename)s` due to"
|
||||||
|
" %(exception)s") %
|
||||||
|
{
|
||||||
|
'filename': self.fname,
|
||||||
|
'exception': e,
|
||||||
|
})
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __enter__(self):
|
||||||
|
self.acquire()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def release(self):
|
||||||
try:
|
try:
|
||||||
self.unlock()
|
self.unlock()
|
||||||
self.lockfile.close()
|
self.lockfile.close()
|
||||||
|
LOG.debug('Released file lock "%s"', self.fname)
|
||||||
except IOError:
|
except IOError:
|
||||||
LOG.exception(_("Could not release the acquired lock `%s`"),
|
LOG.exception(_LE("Could not release the acquired lock `%s`"),
|
||||||
self.fname)
|
self.fname)
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return os.path.exists(self.fname)
|
||||||
|
|
||||||
def trylock(self):
|
def trylock(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -112,7 +132,7 @@ class _InterProcessLock(object):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class _WindowsLock(_InterProcessLock):
|
class _WindowsLock(_FileLock):
|
||||||
def trylock(self):
|
def trylock(self):
|
||||||
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
|
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
|
||||||
|
|
||||||
@ -120,7 +140,7 @@ class _WindowsLock(_InterProcessLock):
|
|||||||
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
|
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
|
||||||
|
|
||||||
|
|
||||||
class _PosixLock(_InterProcessLock):
|
class _FcntlLock(_FileLock):
|
||||||
def trylock(self):
|
def trylock(self):
|
||||||
fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
|
||||||
@ -128,17 +148,122 @@ class _PosixLock(_InterProcessLock):
|
|||||||
fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
|
fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
|
||||||
|
|
||||||
|
|
||||||
|
class _PosixLock(object):
|
||||||
|
def __init__(self, name):
|
||||||
|
# Hash the name because it's not valid to have POSIX semaphore
|
||||||
|
# names with things like / in them. Then use base64 to encode
|
||||||
|
# the digest() instead taking the hexdigest() because the
|
||||||
|
# result is shorter and most systems can't have shm sempahore
|
||||||
|
# names longer than 31 characters.
|
||||||
|
h = hashlib.sha1()
|
||||||
|
h.update(name.encode('ascii'))
|
||||||
|
self.name = str((b'/' + base64.urlsafe_b64encode(
|
||||||
|
h.digest())).decode('ascii'))
|
||||||
|
|
||||||
|
def acquire(self, timeout=None):
|
||||||
|
self.semaphore = posix_ipc.Semaphore(self.name,
|
||||||
|
flags=posix_ipc.O_CREAT,
|
||||||
|
initial_value=1)
|
||||||
|
self.semaphore.acquire(timeout)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.acquire()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
self.semaphore.release()
|
||||||
|
self.semaphore.close()
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
try:
|
||||||
|
semaphore = posix_ipc.Semaphore(self.name)
|
||||||
|
except posix_ipc.ExistentialError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
semaphore.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
import msvcrt
|
import msvcrt
|
||||||
InterProcessLock = _WindowsLock
|
InterProcessLock = _WindowsLock
|
||||||
|
FileLock = _WindowsLock
|
||||||
else:
|
else:
|
||||||
|
import base64
|
||||||
import fcntl
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import posix_ipc
|
||||||
InterProcessLock = _PosixLock
|
InterProcessLock = _PosixLock
|
||||||
|
FileLock = _FcntlLock
|
||||||
|
|
||||||
_semaphores = weakref.WeakValueDictionary()
|
_semaphores = weakref.WeakValueDictionary()
|
||||||
_semaphores_lock = threading.Lock()
|
_semaphores_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lock_path(name, lock_file_prefix, lock_path=None):
|
||||||
|
# NOTE(mikal): the lock name cannot contain directory
|
||||||
|
# separators
|
||||||
|
name = name.replace(os.sep, '_')
|
||||||
|
if lock_file_prefix:
|
||||||
|
sep = '' if lock_file_prefix.endswith('-') else '-'
|
||||||
|
name = '%s%s%s' % (lock_file_prefix, sep, name)
|
||||||
|
|
||||||
|
local_lock_path = lock_path or CONF.lock_path
|
||||||
|
|
||||||
|
if not local_lock_path:
|
||||||
|
# NOTE(bnemec): Create a fake lock path for posix locks so we don't
|
||||||
|
# unnecessarily raise the RequiredOptError below.
|
||||||
|
if InterProcessLock is not _PosixLock:
|
||||||
|
raise cfg.RequiredOptError('lock_path')
|
||||||
|
local_lock_path = 'posixlock:/'
|
||||||
|
|
||||||
|
return os.path.join(local_lock_path, name)
|
||||||
|
|
||||||
|
|
||||||
|
def external_lock(name, lock_file_prefix=None, lock_path=None):
|
||||||
|
LOG.debug('Attempting to grab external lock "%(lock)s"',
|
||||||
|
{'lock': name})
|
||||||
|
|
||||||
|
lock_file_path = _get_lock_path(name, lock_file_prefix, lock_path)
|
||||||
|
|
||||||
|
# NOTE(bnemec): If an explicit lock_path was passed to us then it
|
||||||
|
# means the caller is relying on file-based locking behavior, so
|
||||||
|
# we can't use posix locks for those calls.
|
||||||
|
if lock_path:
|
||||||
|
return FileLock(lock_file_path)
|
||||||
|
return InterProcessLock(lock_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_external_lock_file(name, lock_file_prefix=None):
|
||||||
|
"""Remove an external lock file when it's not used anymore
|
||||||
|
This will be helpful when we have a lot of lock files
|
||||||
|
"""
|
||||||
|
with internal_lock(name):
|
||||||
|
lock_file_path = _get_lock_path(name, lock_file_prefix)
|
||||||
|
try:
|
||||||
|
os.remove(lock_file_path)
|
||||||
|
except OSError:
|
||||||
|
LOG.info(_LI('Failed to remove file %(file)s'),
|
||||||
|
{'file': lock_file_path})
|
||||||
|
|
||||||
|
|
||||||
|
def internal_lock(name):
|
||||||
|
with _semaphores_lock:
|
||||||
|
try:
|
||||||
|
sem = _semaphores[name]
|
||||||
|
except KeyError:
|
||||||
|
sem = threading.Semaphore()
|
||||||
|
_semaphores[name] = sem
|
||||||
|
|
||||||
|
LOG.debug('Got semaphore "%(lock)s"', {'lock': name})
|
||||||
|
return sem
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def lock(name, lock_file_prefix=None, external=False, lock_path=None):
|
def lock(name, lock_file_prefix=None, external=False, lock_path=None):
|
||||||
"""Context based lock
|
"""Context based lock
|
||||||
@ -148,73 +273,22 @@ def lock(name, lock_file_prefix=None, external=False, lock_path=None):
|
|||||||
True, in which case, it'll yield an InterProcessLock instance.
|
True, in which case, it'll yield an InterProcessLock instance.
|
||||||
|
|
||||||
:param lock_file_prefix: The lock_file_prefix argument is used to provide
|
:param lock_file_prefix: The lock_file_prefix argument is used to provide
|
||||||
lock files on disk with a meaningful prefix.
|
lock files on disk with a meaningful prefix.
|
||||||
|
|
||||||
:param external: The external keyword argument denotes whether this lock
|
:param external: The external keyword argument denotes whether this lock
|
||||||
should work across multiple processes. This means that if two different
|
should work across multiple processes. This means that if two different
|
||||||
workers both run a a method decorated with @synchronized('mylock',
|
workers both run a method decorated with @synchronized('mylock',
|
||||||
external=True), only one of them will execute at a time.
|
external=True), only one of them will execute at a time.
|
||||||
|
|
||||||
:param lock_path: The lock_path keyword argument is used to specify a
|
|
||||||
special location for external lock files to live. If nothing is set, then
|
|
||||||
CONF.lock_path is used as a default.
|
|
||||||
"""
|
"""
|
||||||
with _semaphores_lock:
|
int_lock = internal_lock(name)
|
||||||
try:
|
with int_lock:
|
||||||
sem = _semaphores[name]
|
if external and not CONF.disable_process_locking:
|
||||||
except KeyError:
|
ext_lock = external_lock(name, lock_file_prefix, lock_path)
|
||||||
sem = threading.Semaphore()
|
with ext_lock:
|
||||||
_semaphores[name] = sem
|
yield ext_lock
|
||||||
|
else:
|
||||||
with sem:
|
yield int_lock
|
||||||
LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
|
LOG.debug('Released semaphore "%(lock)s"', {'lock': name})
|
||||||
|
|
||||||
# NOTE(mikal): I know this looks odd
|
|
||||||
if not hasattr(local.strong_store, 'locks_held'):
|
|
||||||
local.strong_store.locks_held = []
|
|
||||||
local.strong_store.locks_held.append(name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if external and not CONF.disable_process_locking:
|
|
||||||
LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
|
|
||||||
{'lock': name})
|
|
||||||
|
|
||||||
# We need a copy of lock_path because it is non-local
|
|
||||||
local_lock_path = lock_path or CONF.lock_path
|
|
||||||
if not local_lock_path:
|
|
||||||
raise cfg.RequiredOptError('lock_path')
|
|
||||||
|
|
||||||
if not os.path.exists(local_lock_path):
|
|
||||||
fileutils.ensure_tree(local_lock_path)
|
|
||||||
LOG.info(_('Created lock path: %s'), local_lock_path)
|
|
||||||
|
|
||||||
def add_prefix(name, prefix):
|
|
||||||
if not prefix:
|
|
||||||
return name
|
|
||||||
sep = '' if prefix.endswith('-') else '-'
|
|
||||||
return '%s%s%s' % (prefix, sep, name)
|
|
||||||
|
|
||||||
# NOTE(mikal): the lock name cannot contain directory
|
|
||||||
# separators
|
|
||||||
lock_file_name = add_prefix(name.replace(os.sep, '_'),
|
|
||||||
lock_file_prefix)
|
|
||||||
|
|
||||||
lock_file_path = os.path.join(local_lock_path, lock_file_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
lock = InterProcessLock(lock_file_path)
|
|
||||||
with lock as lock:
|
|
||||||
LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
|
|
||||||
{'lock': name, 'path': lock_file_path})
|
|
||||||
yield lock
|
|
||||||
finally:
|
|
||||||
LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
|
|
||||||
{'lock': name, 'path': lock_file_path})
|
|
||||||
else:
|
|
||||||
yield sem
|
|
||||||
|
|
||||||
finally:
|
|
||||||
local.strong_store.locks_held.remove(name)
|
|
||||||
|
|
||||||
|
|
||||||
def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
|
def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
|
||||||
@ -246,11 +320,11 @@ def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
|
|||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
with lock(name, lock_file_prefix, external, lock_path):
|
with lock(name, lock_file_prefix, external, lock_path):
|
||||||
LOG.debug(_('Got semaphore / lock "%(function)s"'),
|
LOG.debug('Got semaphore / lock "%(function)s"',
|
||||||
{'function': f.__name__})
|
{'function': f.__name__})
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
finally:
|
finally:
|
||||||
LOG.debug(_('Semaphore / lock released "%(function)s"'),
|
LOG.debug('Semaphore / lock released "%(function)s"',
|
||||||
{'function': f.__name__})
|
{'function': f.__name__})
|
||||||
return inner
|
return inner
|
||||||
return wrap
|
return wrap
|
||||||
|
@ -33,7 +33,6 @@ import logging
|
|||||||
import logging.config
|
import logging.config
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -41,31 +40,19 @@ from oslo.config import cfg
|
|||||||
import six
|
import six
|
||||||
from six import moves
|
from six import moves
|
||||||
|
|
||||||
|
_PY26 = sys.version_info[0:2] == (2, 6)
|
||||||
|
|
||||||
from ironic.openstack.common.gettextutils import _
|
from ironic.openstack.common.gettextutils import _
|
||||||
from ironic.openstack.common import importutils
|
from ironic.openstack.common import importutils
|
||||||
from ironic.openstack.common import jsonutils
|
from ironic.openstack.common import jsonutils
|
||||||
from ironic.openstack.common import local
|
from ironic.openstack.common import local
|
||||||
|
# NOTE(flaper87): Pls, remove when graduating this module
|
||||||
|
# from the incubator.
|
||||||
|
from ironic.openstack.common.strutils import mask_password # noqa
|
||||||
|
|
||||||
|
|
||||||
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
|
|
||||||
|
|
||||||
# NOTE(ldbragst): Let's build a list of regex objects using the list of
|
|
||||||
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
|
|
||||||
# to the list of _SANITIZE_KEYS and we can generate regular expressions
|
|
||||||
# for XML and JSON automatically.
|
|
||||||
_SANITIZE_PATTERNS = []
|
|
||||||
_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
|
|
||||||
r'(<%(key)s>).*?(</%(key)s>)',
|
|
||||||
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
|
|
||||||
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])']
|
|
||||||
|
|
||||||
for key in _SANITIZE_KEYS:
|
|
||||||
for pattern in _FORMAT_PATTERNS:
|
|
||||||
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
|
|
||||||
_SANITIZE_PATTERNS.append(reg_ex)
|
|
||||||
|
|
||||||
|
|
||||||
common_cli_opts = [
|
common_cli_opts = [
|
||||||
cfg.BoolOpt('debug',
|
cfg.BoolOpt('debug',
|
||||||
@ -84,14 +71,11 @@ logging_cli_opts = [
|
|||||||
cfg.StrOpt('log-config-append',
|
cfg.StrOpt('log-config-append',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
deprecated_name='log-config',
|
deprecated_name='log-config',
|
||||||
help='The name of logging configuration file. It does not '
|
help='The name of a logging configuration file. This file '
|
||||||
'disable existing loggers, but just appends specified '
|
'is appended to any existing logging configuration '
|
||||||
'logging configuration to any other existing logging '
|
'files. For details about logging configuration files, '
|
||||||
'options. Please see the Python logging module '
|
'see the Python logging module documentation.'),
|
||||||
'documentation for details on logging configuration '
|
|
||||||
'files.'),
|
|
||||||
cfg.StrOpt('log-format',
|
cfg.StrOpt('log-format',
|
||||||
default=None,
|
|
||||||
metavar='FORMAT',
|
metavar='FORMAT',
|
||||||
help='DEPRECATED. '
|
help='DEPRECATED. '
|
||||||
'A logging.Formatter log message format string which may '
|
'A logging.Formatter log message format string which may '
|
||||||
@ -103,7 +87,7 @@ logging_cli_opts = [
|
|||||||
default=_DEFAULT_LOG_DATE_FORMAT,
|
default=_DEFAULT_LOG_DATE_FORMAT,
|
||||||
metavar='DATE_FORMAT',
|
metavar='DATE_FORMAT',
|
||||||
help='Format string for %%(asctime)s in log records. '
|
help='Format string for %%(asctime)s in log records. '
|
||||||
'Default: %(default)s'),
|
'Default: %(default)s .'),
|
||||||
cfg.StrOpt('log-file',
|
cfg.StrOpt('log-file',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
deprecated_name='logfile',
|
deprecated_name='logfile',
|
||||||
@ -112,80 +96,76 @@ logging_cli_opts = [
|
|||||||
cfg.StrOpt('log-dir',
|
cfg.StrOpt('log-dir',
|
||||||
deprecated_name='logdir',
|
deprecated_name='logdir',
|
||||||
help='(Optional) The base directory used for relative '
|
help='(Optional) The base directory used for relative '
|
||||||
'--log-file paths'),
|
'--log-file paths.'),
|
||||||
cfg.BoolOpt('use-syslog',
|
cfg.BoolOpt('use-syslog',
|
||||||
default=False,
|
default=False,
|
||||||
help='Use syslog for logging. '
|
help='Use syslog for logging. '
|
||||||
'Existing syslog format is DEPRECATED during I, '
|
'Existing syslog format is DEPRECATED during I, '
|
||||||
'and then will be changed in J to honor RFC5424'),
|
'and will change in J to honor RFC5424.'),
|
||||||
cfg.BoolOpt('use-syslog-rfc-format',
|
cfg.BoolOpt('use-syslog-rfc-format',
|
||||||
# TODO(bogdando) remove or use True after existing
|
# TODO(bogdando) remove or use True after existing
|
||||||
# syslog format deprecation in J
|
# syslog format deprecation in J
|
||||||
default=False,
|
default=False,
|
||||||
help='(Optional) Use syslog rfc5424 format for logging. '
|
help='(Optional) Enables or disables syslog rfc5424 format '
|
||||||
'If enabled, will add APP-NAME (RFC5424) before the '
|
'for logging. If enabled, prefixes the MSG part of the '
|
||||||
'MSG part of the syslog message. The old format '
|
'syslog message with APP-NAME (RFC5424). The '
|
||||||
'without APP-NAME is deprecated in I, '
|
'format without the APP-NAME is deprecated in I, '
|
||||||
'and will be removed in J.'),
|
'and will be removed in J.'),
|
||||||
cfg.StrOpt('syslog-log-facility',
|
cfg.StrOpt('syslog-log-facility',
|
||||||
default='LOG_USER',
|
default='LOG_USER',
|
||||||
help='Syslog facility to receive log lines')
|
help='Syslog facility to receive log lines.')
|
||||||
]
|
]
|
||||||
|
|
||||||
generic_log_opts = [
|
generic_log_opts = [
|
||||||
cfg.BoolOpt('use_stderr',
|
cfg.BoolOpt('use_stderr',
|
||||||
default=True,
|
default=True,
|
||||||
help='Log output to standard error')
|
help='Log output to standard error.')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'boto=WARN',
|
||||||
|
'qpid=WARN', 'sqlalchemy=WARN', 'suds=INFO',
|
||||||
|
'oslo.messaging=INFO', 'iso8601=WARN',
|
||||||
|
'requests.packages.urllib3.connectionpool=WARN',
|
||||||
|
'urllib3.connectionpool=WARN', 'websocket=WARN']
|
||||||
|
|
||||||
log_opts = [
|
log_opts = [
|
||||||
cfg.StrOpt('logging_context_format_string',
|
cfg.StrOpt('logging_context_format_string',
|
||||||
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||||
'%(name)s [%(request_id)s %(user_identity)s] '
|
'%(name)s [%(request_id)s %(user_identity)s] '
|
||||||
'%(instance)s%(message)s',
|
'%(instance)s%(message)s',
|
||||||
help='Format string to use for log messages with context'),
|
help='Format string to use for log messages with context.'),
|
||||||
cfg.StrOpt('logging_default_format_string',
|
cfg.StrOpt('logging_default_format_string',
|
||||||
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||||
'%(name)s [-] %(instance)s%(message)s',
|
'%(name)s [-] %(instance)s%(message)s',
|
||||||
help='Format string to use for log messages without context'),
|
help='Format string to use for log messages without context.'),
|
||||||
cfg.StrOpt('logging_debug_format_suffix',
|
cfg.StrOpt('logging_debug_format_suffix',
|
||||||
default='%(funcName)s %(pathname)s:%(lineno)d',
|
default='%(funcName)s %(pathname)s:%(lineno)d',
|
||||||
help='Data to append to log format when level is DEBUG'),
|
help='Data to append to log format when level is DEBUG.'),
|
||||||
cfg.StrOpt('logging_exception_prefix',
|
cfg.StrOpt('logging_exception_prefix',
|
||||||
default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
|
default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
|
||||||
'%(instance)s',
|
'%(instance)s',
|
||||||
help='Prefix each line of exception output with this format'),
|
help='Prefix each line of exception output with this format.'),
|
||||||
cfg.ListOpt('default_log_levels',
|
cfg.ListOpt('default_log_levels',
|
||||||
default=[
|
default=DEFAULT_LOG_LEVELS,
|
||||||
'amqp=WARN',
|
help='List of logger=LEVEL pairs.'),
|
||||||
'amqplib=WARN',
|
|
||||||
'boto=WARN',
|
|
||||||
'qpid=WARN',
|
|
||||||
'sqlalchemy=WARN',
|
|
||||||
'suds=INFO',
|
|
||||||
'oslo.messaging=INFO',
|
|
||||||
'iso8601=WARN',
|
|
||||||
'requests.packages.urllib3.connectionpool=WARN'
|
|
||||||
],
|
|
||||||
help='List of logger=LEVEL pairs'),
|
|
||||||
cfg.BoolOpt('publish_errors',
|
cfg.BoolOpt('publish_errors',
|
||||||
default=False,
|
default=False,
|
||||||
help='Publish error events'),
|
help='Enables or disables publication of error events.'),
|
||||||
cfg.BoolOpt('fatal_deprecations',
|
cfg.BoolOpt('fatal_deprecations',
|
||||||
default=False,
|
default=False,
|
||||||
help='Make deprecations fatal'),
|
help='Enables or disables fatal status of deprecations.'),
|
||||||
|
|
||||||
# NOTE(mikal): there are two options here because sometimes we are handed
|
# NOTE(mikal): there are two options here because sometimes we are handed
|
||||||
# a full instance (and could include more information), and other times we
|
# a full instance (and could include more information), and other times we
|
||||||
# are just handed a UUID for the instance.
|
# are just handed a UUID for the instance.
|
||||||
cfg.StrOpt('instance_format',
|
cfg.StrOpt('instance_format',
|
||||||
default='[instance: %(uuid)s] ',
|
default='[instance: %(uuid)s] ',
|
||||||
help='If an instance is passed with the log message, format '
|
help='The format for an instance that is passed with the log '
|
||||||
'it like this'),
|
'message.'),
|
||||||
cfg.StrOpt('instance_uuid_format',
|
cfg.StrOpt('instance_uuid_format',
|
||||||
default='[instance: %(uuid)s] ',
|
default='[instance: %(uuid)s] ',
|
||||||
help='If an instance UUID is passed with the log message, '
|
help='The format for an instance UUID that is passed with the '
|
||||||
'format it like this'),
|
'log message.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -244,45 +224,20 @@ def _get_log_file_path(binary=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def mask_password(message, secret="***"):
|
|
||||||
"""Replace password with 'secret' in message.
|
|
||||||
|
|
||||||
:param message: The string which includes security information.
|
|
||||||
:param secret: value with which to replace passwords.
|
|
||||||
:returns: The unicode value of message with the password fields masked.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
>>> mask_password("'adminPass' : 'aaaaa'")
|
|
||||||
"'adminPass' : '***'"
|
|
||||||
>>> mask_password("'admin_pass' : 'aaaaa'")
|
|
||||||
"'admin_pass' : '***'"
|
|
||||||
>>> mask_password('"password" : "aaaaa"')
|
|
||||||
'"password" : "***"'
|
|
||||||
>>> mask_password("'original_password' : 'aaaaa'")
|
|
||||||
"'original_password' : '***'"
|
|
||||||
>>> mask_password("u'original_password' : u'aaaaa'")
|
|
||||||
"u'original_password' : u'***'"
|
|
||||||
"""
|
|
||||||
message = six.text_type(message)
|
|
||||||
|
|
||||||
# NOTE(ldbragst): Check to see if anything in message contains any key
|
|
||||||
# specified in _SANITIZE_KEYS, if not then just return the message since
|
|
||||||
# we don't have to mask any passwords.
|
|
||||||
if not any(key in message for key in _SANITIZE_KEYS):
|
|
||||||
return message
|
|
||||||
|
|
||||||
secret = r'\g<1>' + secret + r'\g<2>'
|
|
||||||
for pattern in _SANITIZE_PATTERNS:
|
|
||||||
message = re.sub(pattern, secret, message)
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLoggerAdapter(logging.LoggerAdapter):
|
class BaseLoggerAdapter(logging.LoggerAdapter):
|
||||||
|
|
||||||
def audit(self, msg, *args, **kwargs):
|
def audit(self, msg, *args, **kwargs):
|
||||||
self.log(logging.AUDIT, msg, *args, **kwargs)
|
self.log(logging.AUDIT, msg, *args, **kwargs)
|
||||||
|
|
||||||
|
def isEnabledFor(self, level):
|
||||||
|
if _PY26:
|
||||||
|
# This method was added in python 2.7 (and it does the exact
|
||||||
|
# same logic, so we need to do the exact same logic so that
|
||||||
|
# python 2.6 has this capability as well).
|
||||||
|
return self.logger.isEnabledFor(level)
|
||||||
|
else:
|
||||||
|
return super(BaseLoggerAdapter, self).isEnabledFor(level)
|
||||||
|
|
||||||
|
|
||||||
class LazyAdapter(BaseLoggerAdapter):
|
class LazyAdapter(BaseLoggerAdapter):
|
||||||
def __init__(self, name='unknown', version='unknown'):
|
def __init__(self, name='unknown', version='unknown'):
|
||||||
@ -295,6 +250,11 @@ class LazyAdapter(BaseLoggerAdapter):
|
|||||||
def logger(self):
|
def logger(self):
|
||||||
if not self._logger:
|
if not self._logger:
|
||||||
self._logger = getLogger(self.name, self.version)
|
self._logger = getLogger(self.name, self.version)
|
||||||
|
if six.PY3:
|
||||||
|
# In Python 3, the code fails because the 'manager' attribute
|
||||||
|
# cannot be found when using a LoggerAdapter as the
|
||||||
|
# underlying logger. Work around this issue.
|
||||||
|
self._logger.manager = self._logger.logger.manager
|
||||||
return self._logger
|
return self._logger
|
||||||
|
|
||||||
|
|
||||||
@ -424,9 +384,7 @@ class JSONFormatter(logging.Formatter):
|
|||||||
|
|
||||||
def _create_logging_excepthook(product_name):
|
def _create_logging_excepthook(product_name):
|
||||||
def logging_excepthook(exc_type, value, tb):
|
def logging_excepthook(exc_type, value, tb):
|
||||||
extra = {}
|
extra = {'exc_info': (exc_type, value, tb)}
|
||||||
if CONF.verbose or CONF.debug:
|
|
||||||
extra['exc_info'] = (exc_type, value, tb)
|
|
||||||
getLogger(product_name).critical(
|
getLogger(product_name).critical(
|
||||||
"".join(traceback.format_exception_only(exc_type, value)),
|
"".join(traceback.format_exception_only(exc_type, value)),
|
||||||
**extra)
|
**extra)
|
||||||
@ -450,8 +408,8 @@ def _load_log_config(log_config_append):
|
|||||||
try:
|
try:
|
||||||
logging.config.fileConfig(log_config_append,
|
logging.config.fileConfig(log_config_append,
|
||||||
disable_existing_loggers=False)
|
disable_existing_loggers=False)
|
||||||
except moves.configparser.Error as exc:
|
except (moves.configparser.Error, KeyError) as exc:
|
||||||
raise LogConfigError(log_config_append, str(exc))
|
raise LogConfigError(log_config_append, six.text_type(exc))
|
||||||
|
|
||||||
|
|
||||||
def setup(product_name, version='unknown'):
|
def setup(product_name, version='unknown'):
|
||||||
@ -463,10 +421,20 @@ def setup(product_name, version='unknown'):
|
|||||||
sys.excepthook = _create_logging_excepthook(product_name)
|
sys.excepthook = _create_logging_excepthook(product_name)
|
||||||
|
|
||||||
|
|
||||||
def set_defaults(logging_context_format_string):
|
def set_defaults(logging_context_format_string=None,
|
||||||
cfg.set_defaults(log_opts,
|
default_log_levels=None):
|
||||||
logging_context_format_string=
|
# Just in case the caller is not setting the
|
||||||
logging_context_format_string)
|
# default_log_level. This is insurance because
|
||||||
|
# we introduced the default_log_level parameter
|
||||||
|
# later in a backwards in-compatible change
|
||||||
|
if default_log_levels is not None:
|
||||||
|
cfg.set_defaults(
|
||||||
|
log_opts,
|
||||||
|
default_log_levels=default_log_levels)
|
||||||
|
if logging_context_format_string is not None:
|
||||||
|
cfg.set_defaults(
|
||||||
|
log_opts,
|
||||||
|
logging_context_format_string=logging_context_format_string)
|
||||||
|
|
||||||
|
|
||||||
def _find_facility_from_conf():
|
def _find_facility_from_conf():
|
||||||
@ -496,10 +464,16 @@ def _find_facility_from_conf():
|
|||||||
class RFCSysLogHandler(logging.handlers.SysLogHandler):
|
class RFCSysLogHandler(logging.handlers.SysLogHandler):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.binary_name = _get_binary_name()
|
self.binary_name = _get_binary_name()
|
||||||
super(RFCSysLogHandler, self).__init__(*args, **kwargs)
|
# Do not use super() unless type(logging.handlers.SysLogHandler)
|
||||||
|
# is 'type' (Python 2.7).
|
||||||
|
# Use old style calls, if the type is 'classobj' (Python 2.6)
|
||||||
|
logging.handlers.SysLogHandler.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
msg = super(RFCSysLogHandler, self).format(record)
|
# Do not use super() unless type(logging.handlers.SysLogHandler)
|
||||||
|
# is 'type' (Python 2.7).
|
||||||
|
# Use old style calls, if the type is 'classobj' (Python 2.6)
|
||||||
|
msg = logging.handlers.SysLogHandler.format(self, record)
|
||||||
msg = self.binary_name + ' ' + msg
|
msg = self.binary_name + ' ' + msg
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@ -537,9 +511,14 @@ def _setup_logging_from_conf(project, version):
|
|||||||
log_root.addHandler(streamlog)
|
log_root.addHandler(streamlog)
|
||||||
|
|
||||||
if CONF.publish_errors:
|
if CONF.publish_errors:
|
||||||
handler = importutils.import_object(
|
try:
|
||||||
"ironic.openstack.common.log_handler.PublishErrorsHandler",
|
handler = importutils.import_object(
|
||||||
logging.ERROR)
|
"ironic.openstack.common.log_handler.PublishErrorsHandler",
|
||||||
|
logging.ERROR)
|
||||||
|
except ImportError:
|
||||||
|
handler = importutils.import_object(
|
||||||
|
"oslo.messaging.notify.log_handler.PublishErrorsHandler",
|
||||||
|
logging.ERROR)
|
||||||
log_root.addHandler(handler)
|
log_root.addHandler(handler)
|
||||||
|
|
||||||
datefmt = CONF.log_date_format
|
datefmt = CONF.log_date_format
|
||||||
@ -565,9 +544,15 @@ def _setup_logging_from_conf(project, version):
|
|||||||
|
|
||||||
for pair in CONF.default_log_levels:
|
for pair in CONF.default_log_levels:
|
||||||
mod, _sep, level_name = pair.partition('=')
|
mod, _sep, level_name = pair.partition('=')
|
||||||
level = logging.getLevelName(level_name)
|
|
||||||
logger = logging.getLogger(mod)
|
logger = logging.getLogger(mod)
|
||||||
logger.setLevel(level)
|
# NOTE(AAzza) in python2.6 Logger.setLevel doesn't convert string name
|
||||||
|
# to integer code.
|
||||||
|
if sys.version_info < (2, 7):
|
||||||
|
level = logging.getLevelName(level_name)
|
||||||
|
logger.setLevel(level)
|
||||||
|
else:
|
||||||
|
logger.setLevel(level_name)
|
||||||
|
|
||||||
|
|
||||||
_loggers = {}
|
_loggers = {}
|
||||||
|
|
||||||
@ -656,14 +641,19 @@ class ContextFormatter(logging.Formatter):
|
|||||||
record.__dict__[key] = ''
|
record.__dict__[key] = ''
|
||||||
|
|
||||||
if record.__dict__.get('request_id'):
|
if record.__dict__.get('request_id'):
|
||||||
self._fmt = CONF.logging_context_format_string
|
fmt = CONF.logging_context_format_string
|
||||||
else:
|
else:
|
||||||
self._fmt = CONF.logging_default_format_string
|
fmt = CONF.logging_default_format_string
|
||||||
|
|
||||||
if (record.levelno == logging.DEBUG and
|
if (record.levelno == logging.DEBUG and
|
||||||
CONF.logging_debug_format_suffix):
|
CONF.logging_debug_format_suffix):
|
||||||
self._fmt += " " + CONF.logging_debug_format_suffix
|
fmt += " " + CONF.logging_debug_format_suffix
|
||||||
|
|
||||||
|
if sys.version_info < (3, 2):
|
||||||
|
self._fmt = fmt
|
||||||
|
else:
|
||||||
|
self._style = logging.PercentStyle(fmt)
|
||||||
|
self._fmt = self._style._fmt
|
||||||
# Cache this on the record, Logger will respect our formatted copy
|
# Cache this on the record, Logger will respect our formatted copy
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
record.exc_text = self.formatException(record.exc_info, record)
|
record.exc_text = self.formatException(record.exc_info, record)
|
||||||
|
@ -16,31 +16,36 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
from eventlet import event
|
from eventlet import event
|
||||||
from eventlet import greenthread
|
from eventlet import greenthread
|
||||||
|
|
||||||
from ironic.openstack.common.gettextutils import _LE, _LW
|
from ironic.openstack.common.gettextutils import _LE, _LW
|
||||||
from ironic.openstack.common import log as logging
|
from ironic.openstack.common import log as logging
|
||||||
from ironic.openstack.common import timeutils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# NOTE(zyluo): This lambda function was declared to avoid mocking collisions
|
||||||
|
# with time.time() called in the standard logging module
|
||||||
|
# during unittests.
|
||||||
|
_ts = lambda: time.time()
|
||||||
|
|
||||||
|
|
||||||
class LoopingCallDone(Exception):
|
class LoopingCallDone(Exception):
|
||||||
"""Exception to break out and stop a LoopingCall.
|
"""Exception to break out and stop a LoopingCallBase.
|
||||||
|
|
||||||
The poll-function passed to LoopingCall can raise this exception to
|
The poll-function passed to LoopingCallBase can raise this exception to
|
||||||
break out of the loop normally. This is somewhat analogous to
|
break out of the loop normally. This is somewhat analogous to
|
||||||
StopIteration.
|
StopIteration.
|
||||||
|
|
||||||
An optional return-value can be included as the argument to the exception;
|
An optional return-value can be included as the argument to the exception;
|
||||||
this return-value will be returned by LoopingCall.wait()
|
this return-value will be returned by LoopingCallBase.wait()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, retvalue=True):
|
def __init__(self, retvalue=True):
|
||||||
""":param retvalue: Value that LoopingCall.wait() should return."""
|
""":param retvalue: Value that LoopingCallBase.wait() should return."""
|
||||||
self.retvalue = retvalue
|
self.retvalue = retvalue
|
||||||
|
|
||||||
|
|
||||||
@ -72,16 +77,17 @@ class FixedIntervalLoopingCall(LoopingCallBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while self._running:
|
while self._running:
|
||||||
start = timeutils.utcnow()
|
start = _ts()
|
||||||
self.f(*self.args, **self.kw)
|
self.f(*self.args, **self.kw)
|
||||||
end = timeutils.utcnow()
|
end = _ts()
|
||||||
if not self._running:
|
if not self._running:
|
||||||
break
|
break
|
||||||
delay = interval - timeutils.delta_seconds(start, end)
|
delay = end - start - interval
|
||||||
if delay <= 0:
|
if delay > 0:
|
||||||
LOG.warn(_LW('task run outlasted interval by %s sec') %
|
LOG.warn(_LW('task %(func_name)s run outlasted '
|
||||||
-delay)
|
'interval by %(delay).2f sec'),
|
||||||
greenthread.sleep(delay if delay > 0 else 0)
|
{'func_name': repr(self.f), 'delay': delay})
|
||||||
|
greenthread.sleep(-delay if delay < 0 else 0)
|
||||||
except LoopingCallDone as e:
|
except LoopingCallDone as e:
|
||||||
self.stop()
|
self.stop()
|
||||||
done.send(e.retvalue)
|
done.send(e.retvalue)
|
||||||
@ -98,11 +104,6 @@ class FixedIntervalLoopingCall(LoopingCallBase):
|
|||||||
return self.done
|
return self.done
|
||||||
|
|
||||||
|
|
||||||
# TODO(mikal): this class name is deprecated in Havana and should be removed
|
|
||||||
# in the I release
|
|
||||||
LoopingCall = FixedIntervalLoopingCall
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicLoopingCall(LoopingCallBase):
|
class DynamicLoopingCall(LoopingCallBase):
|
||||||
"""A looping call which sleeps until the next known event.
|
"""A looping call which sleeps until the next known event.
|
||||||
|
|
||||||
@ -126,8 +127,9 @@ class DynamicLoopingCall(LoopingCallBase):
|
|||||||
|
|
||||||
if periodic_interval_max is not None:
|
if periodic_interval_max is not None:
|
||||||
idle = min(idle, periodic_interval_max)
|
idle = min(idle, periodic_interval_max)
|
||||||
LOG.debug('Dynamic looping call sleeping for %.02f '
|
LOG.debug('Dynamic looping call %(func_name)s sleeping '
|
||||||
'seconds', idle)
|
'for %(idle).02f seconds',
|
||||||
|
{'func_name': repr(self.f), 'idle': idle})
|
||||||
greenthread.sleep(idle)
|
greenthread.sleep(idle)
|
||||||
except LoopingCallDone as e:
|
except LoopingCallDone as e:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2012 OpenStack Foundation.
|
# Copyright 2012 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -19,15 +17,19 @@
|
|||||||
Network-related utilities and helper functions.
|
Network-related utilities and helper functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ironic.openstack.common import log as logging
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from ironic.openstack.common.gettextutils import _LW
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def parse_host_port(address, default_port=None):
|
def parse_host_port(address, default_port=None):
|
||||||
"""
|
"""Interpret a string as a host:port pair.
|
||||||
Interpret a string as a host:port pair.
|
|
||||||
An IPv6 address MUST be escaped if accompanied by a port,
|
An IPv6 address MUST be escaped if accompanied by a port,
|
||||||
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
|
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
|
||||||
means both [2001:db8:85a3::8a2e:370:7334] and
|
means both [2001:db8:85a3::8a2e:370:7334] and
|
||||||
@ -47,8 +49,12 @@ def parse_host_port(address, default_port=None):
|
|||||||
('::1', 1234)
|
('::1', 1234)
|
||||||
>>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234)
|
>>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234)
|
||||||
('2001:db8:85a3::8a2e:370:7334', 1234)
|
('2001:db8:85a3::8a2e:370:7334', 1234)
|
||||||
|
>>> parse_host_port(None)
|
||||||
|
(None, None)
|
||||||
"""
|
"""
|
||||||
|
if not address:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
if address[0] == '[':
|
if address[0] == '[':
|
||||||
# Escaped ipv6
|
# Escaped ipv6
|
||||||
_host, _port = address[1:].split(']')
|
_host, _port = address[1:].split(']')
|
||||||
@ -67,3 +73,91 @@ def parse_host_port(address, default_port=None):
|
|||||||
port = default_port
|
port = default_port
|
||||||
|
|
||||||
return (host, None if port is None else int(port))
|
return (host, None if port is None else int(port))
|
||||||
|
|
||||||
|
|
||||||
|
class ModifiedSplitResult(parse.SplitResult):
|
||||||
|
"""Split results class for urlsplit."""
|
||||||
|
|
||||||
|
# NOTE(dims): The functions below are needed for Python 2.6.x.
|
||||||
|
# We can remove these when we drop support for 2.6.x.
|
||||||
|
@property
|
||||||
|
def hostname(self):
|
||||||
|
netloc = self.netloc.split('@', 1)[-1]
|
||||||
|
host, port = parse_host_port(netloc)
|
||||||
|
return host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
netloc = self.netloc.split('@', 1)[-1]
|
||||||
|
host, port = parse_host_port(netloc)
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
def urlsplit(url, scheme='', allow_fragments=True):
|
||||||
|
"""Parse a URL using urlparse.urlsplit(), splitting query and fragments.
|
||||||
|
This function papers over Python issue9374 when needed.
|
||||||
|
|
||||||
|
The parameters are the same as urlparse.urlsplit.
|
||||||
|
"""
|
||||||
|
scheme, netloc, path, query, fragment = parse.urlsplit(
|
||||||
|
url, scheme, allow_fragments)
|
||||||
|
if allow_fragments and '#' in path:
|
||||||
|
path, fragment = path.split('#', 1)
|
||||||
|
if '?' in path:
|
||||||
|
path, query = path.split('?', 1)
|
||||||
|
return ModifiedSplitResult(scheme, netloc,
|
||||||
|
path, query, fragment)
|
||||||
|
|
||||||
|
|
||||||
|
def set_tcp_keepalive(sock, tcp_keepalive=True,
|
||||||
|
tcp_keepidle=None,
|
||||||
|
tcp_keepalive_interval=None,
|
||||||
|
tcp_keepalive_count=None):
|
||||||
|
"""Set values for tcp keepalive parameters
|
||||||
|
|
||||||
|
This function configures tcp keepalive parameters if users wish to do
|
||||||
|
so.
|
||||||
|
|
||||||
|
:param tcp_keepalive: Boolean, turn on or off tcp_keepalive. If users are
|
||||||
|
not sure, this should be True, and default values will be used.
|
||||||
|
|
||||||
|
:param tcp_keepidle: time to wait before starting to send keepalive probes
|
||||||
|
:param tcp_keepalive_interval: time between successive probes, once the
|
||||||
|
initial wait time is over
|
||||||
|
:param tcp_keepalive_count: number of probes to send before the connection
|
||||||
|
is killed
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE(praneshp): Despite keepalive being a tcp concept, the level is
|
||||||
|
# still SOL_SOCKET. This is a quirk.
|
||||||
|
if isinstance(tcp_keepalive, bool):
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, tcp_keepalive)
|
||||||
|
else:
|
||||||
|
raise TypeError("tcp_keepalive must be a boolean")
|
||||||
|
|
||||||
|
if not tcp_keepalive:
|
||||||
|
return
|
||||||
|
|
||||||
|
# These options aren't available in the OS X version of eventlet,
|
||||||
|
# Idle + Count * Interval effectively gives you the total timeout.
|
||||||
|
if tcp_keepidle is not None:
|
||||||
|
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||||
|
sock.setsockopt(socket.IPPROTO_TCP,
|
||||||
|
socket.TCP_KEEPIDLE,
|
||||||
|
tcp_keepidle)
|
||||||
|
else:
|
||||||
|
LOG.warning(_LW('tcp_keepidle not available on your system'))
|
||||||
|
if tcp_keepalive_interval is not None:
|
||||||
|
if hasattr(socket, 'TCP_KEEPINTVL'):
|
||||||
|
sock.setsockopt(socket.IPPROTO_TCP,
|
||||||
|
socket.TCP_KEEPINTVL,
|
||||||
|
tcp_keepalive_interval)
|
||||||
|
else:
|
||||||
|
LOG.warning(_LW('tcp_keepintvl not available on your system'))
|
||||||
|
if tcp_keepalive_count is not None:
|
||||||
|
if hasattr(socket, 'TCP_KEEPCNT'):
|
||||||
|
sock.setsockopt(socket.IPPROTO_TCP,
|
||||||
|
socket.TCP_KEEPCNT,
|
||||||
|
tcp_keepalive_count)
|
||||||
|
else:
|
||||||
|
LOG.warning(_LW('tcp_keepknt not available on your system'))
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -13,22 +11,21 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import datetime
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ironic.openstack.common.gettextutils import _ # noqa
|
from ironic.openstack.common.gettextutils import _, _LE, _LI
|
||||||
from ironic.openstack.common import log as logging
|
from ironic.openstack.common import log as logging
|
||||||
from ironic.openstack.common import timeutils
|
|
||||||
|
|
||||||
|
|
||||||
periodic_opts = [
|
periodic_opts = [
|
||||||
cfg.BoolOpt('run_external_periodic_tasks',
|
cfg.BoolOpt('run_external_periodic_tasks',
|
||||||
default=True,
|
default=True,
|
||||||
help=('Some periodic tasks can be run in a separate process. '
|
help='Some periodic tasks can be run in a separate process. '
|
||||||
'Should we run them here?')),
|
'Should we run them here?'),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -48,8 +45,8 @@ def periodic_task(*args, **kwargs):
|
|||||||
|
|
||||||
This decorator can be used in two ways:
|
This decorator can be used in two ways:
|
||||||
|
|
||||||
1. Without arguments '@periodic_task', this will be run on every cycle
|
1. Without arguments '@periodic_task', this will be run on the default
|
||||||
of the periodic scheduler.
|
interval of 60 seconds.
|
||||||
|
|
||||||
2. With arguments:
|
2. With arguments:
|
||||||
@periodic_task(spacing=N [, run_immediately=[True|False]])
|
@periodic_task(spacing=N [, run_immediately=[True|False]])
|
||||||
@ -80,18 +77,18 @@ def periodic_task(*args, **kwargs):
|
|||||||
if f._periodic_immediate:
|
if f._periodic_immediate:
|
||||||
f._periodic_last_run = None
|
f._periodic_last_run = None
|
||||||
else:
|
else:
|
||||||
f._periodic_last_run = timeutils.utcnow()
|
f._periodic_last_run = time.time()
|
||||||
return f
|
return f
|
||||||
|
|
||||||
# NOTE(sirp): The `if` is necessary to allow the decorator to be used with
|
# NOTE(sirp): The `if` is necessary to allow the decorator to be used with
|
||||||
# and without parens.
|
# and without parenthesis.
|
||||||
#
|
#
|
||||||
# In the 'with-parens' case (with kwargs present), this function needs to
|
# In the 'with-parenthesis' case (with kwargs present), this function needs
|
||||||
# return a decorator function since the interpreter will invoke it like:
|
# to return a decorator function since the interpreter will invoke it like:
|
||||||
#
|
#
|
||||||
# periodic_task(*args, **kwargs)(f)
|
# periodic_task(*args, **kwargs)(f)
|
||||||
#
|
#
|
||||||
# In the 'without-parens' case, the original function will be passed
|
# In the 'without-parenthesis' case, the original function will be passed
|
||||||
# in as the first argument, like:
|
# in as the first argument, like:
|
||||||
#
|
#
|
||||||
# periodic_task(f)
|
# periodic_task(f)
|
||||||
@ -115,11 +112,6 @@ class _PeriodicTasksMeta(type):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
cls._periodic_tasks = []
|
cls._periodic_tasks = []
|
||||||
|
|
||||||
try:
|
|
||||||
cls._periodic_last_run = cls._periodic_last_run.copy()
|
|
||||||
except AttributeError:
|
|
||||||
cls._periodic_last_run = {}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cls._periodic_spacing = cls._periodic_spacing.copy()
|
cls._periodic_spacing = cls._periodic_spacing.copy()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -131,28 +123,54 @@ class _PeriodicTasksMeta(type):
|
|||||||
name = task.__name__
|
name = task.__name__
|
||||||
|
|
||||||
if task._periodic_spacing < 0:
|
if task._periodic_spacing < 0:
|
||||||
LOG.info(_('Skipping periodic task %(task)s because '
|
LOG.info(_LI('Skipping periodic task %(task)s because '
|
||||||
'its interval is negative'),
|
'its interval is negative'),
|
||||||
{'task': name})
|
{'task': name})
|
||||||
continue
|
continue
|
||||||
if not task._periodic_enabled:
|
if not task._periodic_enabled:
|
||||||
LOG.info(_('Skipping periodic task %(task)s because '
|
LOG.info(_LI('Skipping periodic task %(task)s because '
|
||||||
'it is disabled'),
|
'it is disabled'),
|
||||||
{'task': name})
|
{'task': name})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# A periodic spacing of zero indicates that this task should
|
# A periodic spacing of zero indicates that this task should
|
||||||
# be run every pass
|
# be run on the default interval to avoid running too
|
||||||
|
# frequently.
|
||||||
if task._periodic_spacing == 0:
|
if task._periodic_spacing == 0:
|
||||||
task._periodic_spacing = None
|
task._periodic_spacing = DEFAULT_INTERVAL
|
||||||
|
|
||||||
cls._periodic_tasks.append((name, task))
|
cls._periodic_tasks.append((name, task))
|
||||||
cls._periodic_spacing[name] = task._periodic_spacing
|
cls._periodic_spacing[name] = task._periodic_spacing
|
||||||
cls._periodic_last_run[name] = task._periodic_last_run
|
|
||||||
|
|
||||||
|
def _nearest_boundary(last_run, spacing):
|
||||||
|
"""Find nearest boundary which is in the past, which is a multiple of the
|
||||||
|
spacing with the last run as an offset.
|
||||||
|
|
||||||
|
Eg if last run was 10 and spacing was 7, the new last run could be: 17, 24,
|
||||||
|
31, 38...
|
||||||
|
|
||||||
|
0% to 5% of the spacing value will be added to this value to ensure tasks
|
||||||
|
do not synchronize. This jitter is rounded to the nearest second, this
|
||||||
|
means that spacings smaller than 20 seconds will not have jitter.
|
||||||
|
"""
|
||||||
|
current_time = time.time()
|
||||||
|
if last_run is None:
|
||||||
|
return current_time
|
||||||
|
delta = current_time - last_run
|
||||||
|
offset = delta % spacing
|
||||||
|
# Add up to 5% jitter
|
||||||
|
jitter = int(spacing * (random.random() / 20))
|
||||||
|
return current_time - offset + jitter
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(_PeriodicTasksMeta)
|
@six.add_metaclass(_PeriodicTasksMeta)
|
||||||
class PeriodicTasks(object):
|
class PeriodicTasks(object):
|
||||||
|
def __init__(self):
|
||||||
|
super(PeriodicTasks, self).__init__()
|
||||||
|
self._periodic_last_run = {}
|
||||||
|
for name, task in self._periodic_tasks:
|
||||||
|
self._periodic_last_run[name] = task._periodic_last_run
|
||||||
|
|
||||||
def run_periodic_tasks(self, context, raise_on_error=False):
|
def run_periodic_tasks(self, context, raise_on_error=False):
|
||||||
"""Tasks to be run at a periodic interval."""
|
"""Tasks to be run at a periodic interval."""
|
||||||
@ -160,30 +178,28 @@ class PeriodicTasks(object):
|
|||||||
for task_name, task in self._periodic_tasks:
|
for task_name, task in self._periodic_tasks:
|
||||||
full_task_name = '.'.join([self.__class__.__name__, task_name])
|
full_task_name = '.'.join([self.__class__.__name__, task_name])
|
||||||
|
|
||||||
now = timeutils.utcnow()
|
|
||||||
spacing = self._periodic_spacing[task_name]
|
spacing = self._periodic_spacing[task_name]
|
||||||
last_run = self._periodic_last_run[task_name]
|
last_run = self._periodic_last_run[task_name]
|
||||||
|
|
||||||
# If a periodic task is _nearly_ due, then we'll run it early
|
# Check if due, if not skip
|
||||||
if spacing is not None and last_run is not None:
|
idle_for = min(idle_for, spacing)
|
||||||
due = last_run + datetime.timedelta(seconds=spacing)
|
if last_run is not None:
|
||||||
if not timeutils.is_soon(due, 0.2):
|
delta = last_run + spacing - time.time()
|
||||||
idle_for = min(idle_for, timeutils.delta_seconds(now, due))
|
if delta > 0:
|
||||||
|
idle_for = min(idle_for, delta)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if spacing is not None:
|
LOG.debug("Running periodic task %(full_task_name)s",
|
||||||
idle_for = min(idle_for, spacing)
|
|
||||||
|
|
||||||
LOG.debug(_("Running periodic task %(full_task_name)s"),
|
|
||||||
{"full_task_name": full_task_name})
|
{"full_task_name": full_task_name})
|
||||||
self._periodic_last_run[task_name] = timeutils.utcnow()
|
self._periodic_last_run[task_name] = _nearest_boundary(
|
||||||
|
last_run, spacing)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
task(self, context)
|
task(self, context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise
|
raise
|
||||||
LOG.exception(_("Error during %(full_task_name)s: %(e)s"),
|
LOG.exception(_LE("Error during %(full_task_name)s: %(e)s"),
|
||||||
{"full_task_name": full_task_name, "e": e})
|
{"full_task_name": full_task_name, "e": e})
|
||||||
time.sleep(0)
|
time.sleep(0)
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ System-level utilities and helper functions.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import logging as stdlib_logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
@ -29,7 +30,7 @@ from eventlet import greenthread
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from ironic.openstack.common.gettextutils import _
|
from ironic.openstack.common.gettextutils import _
|
||||||
from ironic.openstack.common import log as logging
|
from ironic.openstack.common import strutils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -114,8 +115,7 @@ def execute(*cmd, **kwargs):
|
|||||||
execute this command. Defaults to false.
|
execute this command. Defaults to false.
|
||||||
:type shell: boolean
|
:type shell: boolean
|
||||||
:param loglevel: log level for execute commands.
|
:param loglevel: log level for execute commands.
|
||||||
:type loglevel: int. (Should be stdlib_logging.DEBUG or
|
:type loglevel: int. (Should be logging.DEBUG or logging.INFO)
|
||||||
stdlib_logging.INFO)
|
|
||||||
:returns: (stdout, stderr) from process execution
|
:returns: (stdout, stderr) from process execution
|
||||||
:raises: :class:`UnknownArgumentError` on
|
:raises: :class:`UnknownArgumentError` on
|
||||||
receiving unknown arguments
|
receiving unknown arguments
|
||||||
@ -131,7 +131,7 @@ def execute(*cmd, **kwargs):
|
|||||||
run_as_root = kwargs.pop('run_as_root', False)
|
run_as_root = kwargs.pop('run_as_root', False)
|
||||||
root_helper = kwargs.pop('root_helper', '')
|
root_helper = kwargs.pop('root_helper', '')
|
||||||
shell = kwargs.pop('shell', False)
|
shell = kwargs.pop('shell', False)
|
||||||
loglevel = kwargs.pop('loglevel', stdlib_logging.DEBUG)
|
loglevel = kwargs.pop('loglevel', logging.DEBUG)
|
||||||
|
|
||||||
if isinstance(check_exit_code, bool):
|
if isinstance(check_exit_code, bool):
|
||||||
ignore_exit_code = not check_exit_code
|
ignore_exit_code = not check_exit_code
|
||||||
@ -140,8 +140,7 @@ def execute(*cmd, **kwargs):
|
|||||||
check_exit_code = [check_exit_code]
|
check_exit_code = [check_exit_code]
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
raise UnknownArgumentError(_('Got unknown keyword args '
|
raise UnknownArgumentError(_('Got unknown keyword args: %r') % kwargs)
|
||||||
'to utils.execute: %r') % kwargs)
|
|
||||||
|
|
||||||
if run_as_root and hasattr(os, 'geteuid') and os.geteuid() != 0:
|
if run_as_root and hasattr(os, 'geteuid') and os.geteuid() != 0:
|
||||||
if not root_helper:
|
if not root_helper:
|
||||||
@ -156,7 +155,7 @@ def execute(*cmd, **kwargs):
|
|||||||
attempts -= 1
|
attempts -= 1
|
||||||
try:
|
try:
|
||||||
LOG.log(loglevel, 'Running cmd (subprocess): %s',
|
LOG.log(loglevel, 'Running cmd (subprocess): %s',
|
||||||
' '.join(logging.mask_password(cmd)))
|
strutils.mask_password(' '.join(cmd)))
|
||||||
_PIPE = subprocess.PIPE # pylint: disable=E1101
|
_PIPE = subprocess.PIPE # pylint: disable=E1101
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
@ -270,3 +269,15 @@ def ssh_execute(ssh, cmd, process_input=None,
|
|||||||
cmd=cmd)
|
cmd=cmd)
|
||||||
|
|
||||||
return (stdout, stderr)
|
return (stdout, stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_worker_count():
|
||||||
|
"""Utility to get the default worker count.
|
||||||
|
|
||||||
|
@return: The number of CPUs if that can be determined, else a default
|
||||||
|
worker count of 1 is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return multiprocessing.cpu_count()
|
||||||
|
except NotImplementedError:
|
||||||
|
return 1
|
||||||
|
@ -38,9 +38,10 @@ from eventlet import event
|
|||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
from ironic.openstack.common import eventlet_backdoor
|
from ironic.openstack.common import eventlet_backdoor
|
||||||
from ironic.openstack.common.gettextutils import _
|
from ironic.openstack.common.gettextutils import _LE, _LI, _LW
|
||||||
from ironic.openstack.common import importutils
|
from ironic.openstack.common import importutils
|
||||||
from ironic.openstack.common import log as logging
|
from ironic.openstack.common import log as logging
|
||||||
|
from ironic.openstack.common import systemd
|
||||||
from ironic.openstack.common import threadgroup
|
from ironic.openstack.common import threadgroup
|
||||||
|
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ class ServiceLauncher(Launcher):
|
|||||||
status = None
|
status = None
|
||||||
signo = 0
|
signo = 0
|
||||||
|
|
||||||
LOG.debug(_('Full set of CONF:'))
|
LOG.debug('Full set of CONF:')
|
||||||
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -172,7 +173,7 @@ class ServiceLauncher(Launcher):
|
|||||||
super(ServiceLauncher, self).wait()
|
super(ServiceLauncher, self).wait()
|
||||||
except SignalExit as exc:
|
except SignalExit as exc:
|
||||||
signame = _signo_to_signame(exc.signo)
|
signame = _signo_to_signame(exc.signo)
|
||||||
LOG.info(_('Caught %s, exiting'), signame)
|
LOG.info(_LI('Caught %s, exiting'), signame)
|
||||||
status = exc.code
|
status = exc.code
|
||||||
signo = exc.signo
|
signo = exc.signo
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
@ -184,11 +185,12 @@ class ServiceLauncher(Launcher):
|
|||||||
rpc.cleanup()
|
rpc.cleanup()
|
||||||
except Exception:
|
except Exception:
|
||||||
# We're shutting down, so it doesn't matter at this point.
|
# We're shutting down, so it doesn't matter at this point.
|
||||||
LOG.exception(_('Exception during rpc cleanup.'))
|
LOG.exception(_LE('Exception during rpc cleanup.'))
|
||||||
|
|
||||||
return status, signo
|
return status, signo
|
||||||
|
|
||||||
def wait(self, ready_callback=None):
|
def wait(self, ready_callback=None):
|
||||||
|
systemd.notify_once()
|
||||||
while True:
|
while True:
|
||||||
self.handle_signal()
|
self.handle_signal()
|
||||||
status, signo = self._wait_for_exit_or_signal(ready_callback)
|
status, signo = self._wait_for_exit_or_signal(ready_callback)
|
||||||
@ -235,7 +237,7 @@ class ProcessLauncher(object):
|
|||||||
# dies unexpectedly
|
# dies unexpectedly
|
||||||
self.readpipe.read()
|
self.readpipe.read()
|
||||||
|
|
||||||
LOG.info(_('Parent process has died unexpectedly, exiting'))
|
LOG.info(_LI('Parent process has died unexpectedly, exiting'))
|
||||||
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@ -266,13 +268,13 @@ class ProcessLauncher(object):
|
|||||||
launcher.wait()
|
launcher.wait()
|
||||||
except SignalExit as exc:
|
except SignalExit as exc:
|
||||||
signame = _signo_to_signame(exc.signo)
|
signame = _signo_to_signame(exc.signo)
|
||||||
LOG.info(_('Caught %s, exiting'), signame)
|
LOG.info(_LI('Child caught %s, exiting'), signame)
|
||||||
status = exc.code
|
status = exc.code
|
||||||
signo = exc.signo
|
signo = exc.signo
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
status = exc.code
|
status = exc.code
|
||||||
except BaseException:
|
except BaseException:
|
||||||
LOG.exception(_('Unhandled exception'))
|
LOG.exception(_LE('Unhandled exception'))
|
||||||
status = 2
|
status = 2
|
||||||
finally:
|
finally:
|
||||||
launcher.stop()
|
launcher.stop()
|
||||||
@ -305,7 +307,7 @@ class ProcessLauncher(object):
|
|||||||
# start up quickly but ensure we don't fork off children that
|
# start up quickly but ensure we don't fork off children that
|
||||||
# die instantly too quickly.
|
# die instantly too quickly.
|
||||||
if time.time() - wrap.forktimes[0] < wrap.workers:
|
if time.time() - wrap.forktimes[0] < wrap.workers:
|
||||||
LOG.info(_('Forking too fast, sleeping'))
|
LOG.info(_LI('Forking too fast, sleeping'))
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
wrap.forktimes.pop(0)
|
wrap.forktimes.pop(0)
|
||||||
@ -324,7 +326,7 @@ class ProcessLauncher(object):
|
|||||||
|
|
||||||
os._exit(status)
|
os._exit(status)
|
||||||
|
|
||||||
LOG.info(_('Started child %d'), pid)
|
LOG.info(_LI('Started child %d'), pid)
|
||||||
|
|
||||||
wrap.children.add(pid)
|
wrap.children.add(pid)
|
||||||
self.children[pid] = wrap
|
self.children[pid] = wrap
|
||||||
@ -334,7 +336,7 @@ class ProcessLauncher(object):
|
|||||||
def launch_service(self, service, workers=1):
|
def launch_service(self, service, workers=1):
|
||||||
wrap = ServiceWrapper(service, workers)
|
wrap = ServiceWrapper(service, workers)
|
||||||
|
|
||||||
LOG.info(_('Starting %d workers'), wrap.workers)
|
LOG.info(_LI('Starting %d workers'), wrap.workers)
|
||||||
while self.running and len(wrap.children) < wrap.workers:
|
while self.running and len(wrap.children) < wrap.workers:
|
||||||
self._start_child(wrap)
|
self._start_child(wrap)
|
||||||
|
|
||||||
@ -351,15 +353,15 @@ class ProcessLauncher(object):
|
|||||||
|
|
||||||
if os.WIFSIGNALED(status):
|
if os.WIFSIGNALED(status):
|
||||||
sig = os.WTERMSIG(status)
|
sig = os.WTERMSIG(status)
|
||||||
LOG.info(_('Child %(pid)d killed by signal %(sig)d'),
|
LOG.info(_LI('Child %(pid)d killed by signal %(sig)d'),
|
||||||
dict(pid=pid, sig=sig))
|
dict(pid=pid, sig=sig))
|
||||||
else:
|
else:
|
||||||
code = os.WEXITSTATUS(status)
|
code = os.WEXITSTATUS(status)
|
||||||
LOG.info(_('Child %(pid)s exited with status %(code)d'),
|
LOG.info(_LI('Child %(pid)s exited with status %(code)d'),
|
||||||
dict(pid=pid, code=code))
|
dict(pid=pid, code=code))
|
||||||
|
|
||||||
if pid not in self.children:
|
if pid not in self.children:
|
||||||
LOG.warning(_('pid %d not in child list'), pid)
|
LOG.warning(_LW('pid %d not in child list'), pid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
wrap = self.children.pop(pid)
|
wrap = self.children.pop(pid)
|
||||||
@ -381,23 +383,35 @@ class ProcessLauncher(object):
|
|||||||
def wait(self):
|
def wait(self):
|
||||||
"""Loop waiting on children to die and respawning as necessary."""
|
"""Loop waiting on children to die and respawning as necessary."""
|
||||||
|
|
||||||
LOG.debug(_('Full set of CONF:'))
|
systemd.notify_once()
|
||||||
|
LOG.debug('Full set of CONF:')
|
||||||
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
self.handle_signal()
|
while True:
|
||||||
self._respawn_children()
|
self.handle_signal()
|
||||||
if self.sigcaught:
|
self._respawn_children()
|
||||||
|
# No signal means that stop was called. Don't clean up here.
|
||||||
|
if not self.sigcaught:
|
||||||
|
return
|
||||||
|
|
||||||
signame = _signo_to_signame(self.sigcaught)
|
signame = _signo_to_signame(self.sigcaught)
|
||||||
LOG.info(_('Caught %s, stopping children'), signame)
|
LOG.info(_LI('Caught %s, stopping children'), signame)
|
||||||
if not _is_sighup_and_daemon(self.sigcaught):
|
if not _is_sighup_and_daemon(self.sigcaught):
|
||||||
break
|
break
|
||||||
|
|
||||||
for pid in self.children:
|
for pid in self.children:
|
||||||
os.kill(pid, signal.SIGHUP)
|
os.kill(pid, signal.SIGHUP)
|
||||||
self.running = True
|
self.running = True
|
||||||
self.sigcaught = None
|
self.sigcaught = None
|
||||||
|
except eventlet.greenlet.GreenletExit:
|
||||||
|
LOG.info(_LI("Wait called after thread killed. Cleaning up."))
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Terminate child processes and wait on each."""
|
||||||
|
self.running = False
|
||||||
for pid in self.children:
|
for pid in self.children:
|
||||||
try:
|
try:
|
||||||
os.kill(pid, signal.SIGTERM)
|
os.kill(pid, signal.SIGTERM)
|
||||||
@ -407,7 +421,7 @@ class ProcessLauncher(object):
|
|||||||
|
|
||||||
# Wait for children to die
|
# Wait for children to die
|
||||||
if self.children:
|
if self.children:
|
||||||
LOG.info(_('Waiting on %d children to exit'), len(self.children))
|
LOG.info(_LI('Waiting on %d children to exit'), len(self.children))
|
||||||
while self.children:
|
while self.children:
|
||||||
self._wait_child()
|
self._wait_child()
|
||||||
|
|
||||||
|
106
ironic/openstack/common/systemd.py
Normal file
106
ironic/openstack/common/systemd.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Copyright 2012-2014 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper module for systemd service readiness notification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ironic.openstack.common import log as logging
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _abstractify(socket_name):
|
||||||
|
if socket_name.startswith('@'):
|
||||||
|
# abstract namespace socket
|
||||||
|
socket_name = '\0%s' % socket_name[1:]
|
||||||
|
return socket_name
|
||||||
|
|
||||||
|
|
||||||
|
def _sd_notify(unset_env, msg):
|
||||||
|
notify_socket = os.getenv('NOTIFY_SOCKET')
|
||||||
|
if notify_socket:
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
sock.connect(_abstractify(notify_socket))
|
||||||
|
sock.sendall(msg)
|
||||||
|
if unset_env:
|
||||||
|
del os.environ['NOTIFY_SOCKET']
|
||||||
|
except EnvironmentError:
|
||||||
|
LOG.debug("Systemd notification failed", exc_info=True)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def notify():
|
||||||
|
"""Send notification to Systemd that service is ready.
|
||||||
|
|
||||||
|
For details see
|
||||||
|
http://www.freedesktop.org/software/systemd/man/sd_notify.html
|
||||||
|
"""
|
||||||
|
_sd_notify(False, 'READY=1')
|
||||||
|
|
||||||
|
|
||||||
|
def notify_once():
|
||||||
|
"""Send notification once to Systemd that service is ready.
|
||||||
|
|
||||||
|
Systemd sets NOTIFY_SOCKET environment variable with the name of the
|
||||||
|
socket listening for notifications from services.
|
||||||
|
This method removes the NOTIFY_SOCKET environment variable to ensure
|
||||||
|
notification is sent only once.
|
||||||
|
"""
|
||||||
|
_sd_notify(True, 'READY=1')
|
||||||
|
|
||||||
|
|
||||||
|
def onready(notify_socket, timeout):
|
||||||
|
"""Wait for systemd style notification on the socket.
|
||||||
|
|
||||||
|
:param notify_socket: local socket address
|
||||||
|
:type notify_socket: string
|
||||||
|
:param timeout: socket timeout
|
||||||
|
:type timeout: float
|
||||||
|
:returns: 0 service ready
|
||||||
|
1 service not ready
|
||||||
|
2 timeout occurred
|
||||||
|
"""
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.bind(_abstractify(notify_socket))
|
||||||
|
try:
|
||||||
|
msg = sock.recv(512)
|
||||||
|
except socket.timeout:
|
||||||
|
return 2
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
if 'READY=1' in msg:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# simple CLI for testing
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
notify()
|
||||||
|
elif len(sys.argv) >= 2:
|
||||||
|
timeout = float(sys.argv[1])
|
||||||
|
notify_socket = os.getenv('NOTIFY_SOCKET')
|
||||||
|
if notify_socket:
|
||||||
|
retval = onready(notify_socket, timeout)
|
||||||
|
sys.exit(retval)
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2012 Red Hat, Inc.
|
# Copyright 2012 Red Hat, Inc.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -13,10 +11,10 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import threading
|
||||||
|
|
||||||
from eventlet import greenlet
|
import eventlet
|
||||||
from eventlet import greenpool
|
from eventlet import greenpool
|
||||||
from eventlet import greenthread
|
|
||||||
|
|
||||||
from ironic.openstack.common import log as logging
|
from ironic.openstack.common import log as logging
|
||||||
from ironic.openstack.common import loopingcall
|
from ironic.openstack.common import loopingcall
|
||||||
@ -26,7 +24,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _thread_done(gt, *args, **kwargs):
|
def _thread_done(gt, *args, **kwargs):
|
||||||
""" Callback function to be passed to GreenThread.link() when we spawn()
|
"""Callback function to be passed to GreenThread.link() when we spawn()
|
||||||
Calls the :class:`ThreadGroup` to notify if.
|
Calls the :class:`ThreadGroup` to notify if.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -34,7 +32,7 @@ def _thread_done(gt, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class Thread(object):
|
class Thread(object):
|
||||||
""" Wrapper around a greenthread, that holds a reference to the
|
"""Wrapper around a greenthread, that holds a reference to the
|
||||||
:class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when
|
:class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when
|
||||||
it has done so it can be removed from the threads list.
|
it has done so it can be removed from the threads list.
|
||||||
"""
|
"""
|
||||||
@ -48,9 +46,12 @@ class Thread(object):
|
|||||||
def wait(self):
|
def wait(self):
|
||||||
return self.thread.wait()
|
return self.thread.wait()
|
||||||
|
|
||||||
|
def link(self, func, *args, **kwargs):
|
||||||
|
self.thread.link(func, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ThreadGroup(object):
|
class ThreadGroup(object):
|
||||||
""" The point of the ThreadGroup classis to:
|
"""The point of the ThreadGroup class is to:
|
||||||
|
|
||||||
* keep track of timers and greenthreads (making it easier to stop them
|
* keep track of timers and greenthreads (making it easier to stop them
|
||||||
when need be).
|
when need be).
|
||||||
@ -79,13 +80,17 @@ class ThreadGroup(object):
|
|||||||
gt = self.pool.spawn(callback, *args, **kwargs)
|
gt = self.pool.spawn(callback, *args, **kwargs)
|
||||||
th = Thread(gt, self)
|
th = Thread(gt, self)
|
||||||
self.threads.append(th)
|
self.threads.append(th)
|
||||||
|
return th
|
||||||
|
|
||||||
def thread_done(self, thread):
|
def thread_done(self, thread):
|
||||||
self.threads.remove(thread)
|
self.threads.remove(thread)
|
||||||
|
|
||||||
def stop(self):
|
def _stop_threads(self):
|
||||||
current = greenthread.getcurrent()
|
current = threading.current_thread()
|
||||||
for x in self.threads:
|
|
||||||
|
# Iterate over a copy of self.threads so thread_done doesn't
|
||||||
|
# modify the list while we're iterating
|
||||||
|
for x in self.threads[:]:
|
||||||
if x is current:
|
if x is current:
|
||||||
# don't kill the current thread.
|
# don't kill the current thread.
|
||||||
continue
|
continue
|
||||||
@ -94,6 +99,7 @@ class ThreadGroup(object):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
|
|
||||||
|
def stop_timers(self):
|
||||||
for x in self.timers:
|
for x in self.timers:
|
||||||
try:
|
try:
|
||||||
x.stop()
|
x.stop()
|
||||||
@ -101,21 +107,41 @@ class ThreadGroup(object):
|
|||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
self.timers = []
|
self.timers = []
|
||||||
|
|
||||||
|
def stop(self, graceful=False):
|
||||||
|
"""stop function has the option of graceful=True/False.
|
||||||
|
|
||||||
|
* In case of graceful=True, wait for all threads to be finished.
|
||||||
|
Never kill threads.
|
||||||
|
* In case of graceful=False, kill threads immediately.
|
||||||
|
"""
|
||||||
|
self.stop_timers()
|
||||||
|
if graceful:
|
||||||
|
# In case of graceful=True, wait for all threads to be
|
||||||
|
# finished, never kill threads
|
||||||
|
self.wait()
|
||||||
|
else:
|
||||||
|
# In case of graceful=False(Default), kill threads
|
||||||
|
# immediately
|
||||||
|
self._stop_threads()
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
for x in self.timers:
|
for x in self.timers:
|
||||||
try:
|
try:
|
||||||
x.wait()
|
x.wait()
|
||||||
except greenlet.GreenletExit:
|
except eventlet.greenlet.GreenletExit:
|
||||||
pass
|
pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
current = greenthread.getcurrent()
|
current = threading.current_thread()
|
||||||
for x in self.threads:
|
|
||||||
|
# Iterate over a copy of self.threads so thread_done doesn't
|
||||||
|
# modify the list while we're iterating
|
||||||
|
for x in self.threads[:]:
|
||||||
if x is current:
|
if x is current:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
x.wait()
|
x.wait()
|
||||||
except greenlet.GreenletExit:
|
except eventlet.greenlet.GreenletExit:
|
||||||
pass
|
pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2011 OpenStack Foundation.
|
# Copyright 2011 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -79,6 +77,9 @@ def is_older_than(before, seconds):
|
|||||||
"""Return True if before is older than seconds."""
|
"""Return True if before is older than seconds."""
|
||||||
if isinstance(before, six.string_types):
|
if isinstance(before, six.string_types):
|
||||||
before = parse_strtime(before).replace(tzinfo=None)
|
before = parse_strtime(before).replace(tzinfo=None)
|
||||||
|
else:
|
||||||
|
before = before.replace(tzinfo=None)
|
||||||
|
|
||||||
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
@ -86,6 +87,9 @@ def is_newer_than(after, seconds):
|
|||||||
"""Return True if after is newer than seconds."""
|
"""Return True if after is newer than seconds."""
|
||||||
if isinstance(after, six.string_types):
|
if isinstance(after, six.string_types):
|
||||||
after = parse_strtime(after).replace(tzinfo=None)
|
after = parse_strtime(after).replace(tzinfo=None)
|
||||||
|
else:
|
||||||
|
after = after.replace(tzinfo=None)
|
||||||
|
|
||||||
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
@ -110,7 +114,7 @@ def utcnow():
|
|||||||
|
|
||||||
|
|
||||||
def iso8601_from_timestamp(timestamp):
|
def iso8601_from_timestamp(timestamp):
|
||||||
"""Returns a iso8601 formated date from timestamp."""
|
"""Returns an iso8601 formatted date from timestamp."""
|
||||||
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +134,7 @@ def set_time_override(override_time=None):
|
|||||||
|
|
||||||
def advance_time_delta(timedelta):
|
def advance_time_delta(timedelta):
|
||||||
"""Advance overridden time using a datetime.timedelta."""
|
"""Advance overridden time using a datetime.timedelta."""
|
||||||
assert(not utcnow.override_time is None)
|
assert utcnow.override_time is not None
|
||||||
try:
|
try:
|
||||||
for dt in utcnow.override_time:
|
for dt in utcnow.override_time:
|
||||||
dt += timedelta
|
dt += timedelta
|
||||||
@ -178,6 +182,15 @@ def delta_seconds(before, after):
|
|||||||
datetime objects (as a float, to microsecond resolution).
|
datetime objects (as a float, to microsecond resolution).
|
||||||
"""
|
"""
|
||||||
delta = after - before
|
delta = after - before
|
||||||
|
return total_seconds(delta)
|
||||||
|
|
||||||
|
|
||||||
|
def total_seconds(delta):
|
||||||
|
"""Return the total seconds of datetime.timedelta object.
|
||||||
|
|
||||||
|
Compute total seconds of datetime.timedelta, datetime.timedelta
|
||||||
|
doesn't have method total_seconds in Python2.6, calculate it manually.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return delta.total_seconds()
|
return delta.total_seconds()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -188,8 +201,8 @@ def delta_seconds(before, after):
|
|||||||
def is_soon(dt, window):
|
def is_soon(dt, window):
|
||||||
"""Determines if time is going to happen in the next window seconds.
|
"""Determines if time is going to happen in the next window seconds.
|
||||||
|
|
||||||
:params dt: the time
|
:param dt: the time
|
||||||
:params window: minimum seconds to remain to consider the time not soon
|
:param window: minimum seconds to remain to consider the time not soon
|
||||||
|
|
||||||
:return: True if expiration is within the given duration
|
:return: True if expiration is within the given duration
|
||||||
"""
|
"""
|
||||||
|
37
ironic/openstack/common/uuidutils.py
Normal file
37
ironic/openstack/common/uuidutils.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright (c) 2012 Intel Corporation.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
UUID related utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_uuid():
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def is_uuid_like(val):
|
||||||
|
"""Returns validation of a value as a UUID.
|
||||||
|
|
||||||
|
For our purposes, a UUID is a canonical form string:
|
||||||
|
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return str(uuid.UUID(val)) == val
|
||||||
|
except (TypeError, ValueError, AttributeError):
|
||||||
|
return False
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright (c) 2013 OpenStack Foundation
|
# Copyright (c) 2013 OpenStack Foundation
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -19,8 +17,148 @@
|
|||||||
Helpers for comparing version strings.
|
Helpers for comparing version strings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
from ironic.openstack.common.gettextutils import _
|
||||||
|
from ironic.openstack.common import log as logging
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class deprecated(object):
|
||||||
|
"""A decorator to mark callables as deprecated.
|
||||||
|
|
||||||
|
This decorator logs a deprecation message when the callable it decorates is
|
||||||
|
used. The message will include the release where the callable was
|
||||||
|
deprecated, the release where it may be removed and possibly an optional
|
||||||
|
replacement.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
1. Specifying the required deprecated release
|
||||||
|
|
||||||
|
>>> @deprecated(as_of=deprecated.ICEHOUSE)
|
||||||
|
... def a(): pass
|
||||||
|
|
||||||
|
2. Specifying a replacement:
|
||||||
|
|
||||||
|
>>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()')
|
||||||
|
... def b(): pass
|
||||||
|
|
||||||
|
3. Specifying the release where the functionality may be removed:
|
||||||
|
|
||||||
|
>>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=+1)
|
||||||
|
... def c(): pass
|
||||||
|
|
||||||
|
4. Specifying the deprecated functionality will not be removed:
|
||||||
|
>>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=0)
|
||||||
|
... def d(): pass
|
||||||
|
|
||||||
|
5. Specifying a replacement, deprecated functionality will not be removed:
|
||||||
|
>>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()', remove_in=0)
|
||||||
|
... def e(): pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE(morganfainberg): Bexar is used for unit test purposes, it is
|
||||||
|
# expected we maintain a gap between Bexar and Folsom in this list.
|
||||||
|
BEXAR = 'B'
|
||||||
|
FOLSOM = 'F'
|
||||||
|
GRIZZLY = 'G'
|
||||||
|
HAVANA = 'H'
|
||||||
|
ICEHOUSE = 'I'
|
||||||
|
JUNO = 'J'
|
||||||
|
KILO = 'K'
|
||||||
|
|
||||||
|
_RELEASES = {
|
||||||
|
# NOTE(morganfainberg): Bexar is used for unit test purposes, it is
|
||||||
|
# expected we maintain a gap between Bexar and Folsom in this list.
|
||||||
|
'B': 'Bexar',
|
||||||
|
'F': 'Folsom',
|
||||||
|
'G': 'Grizzly',
|
||||||
|
'H': 'Havana',
|
||||||
|
'I': 'Icehouse',
|
||||||
|
'J': 'Juno',
|
||||||
|
'K': 'Kilo',
|
||||||
|
}
|
||||||
|
|
||||||
|
_deprecated_msg_with_alternative = _(
|
||||||
|
'%(what)s is deprecated as of %(as_of)s in favor of '
|
||||||
|
'%(in_favor_of)s and may be removed in %(remove_in)s.')
|
||||||
|
|
||||||
|
_deprecated_msg_no_alternative = _(
|
||||||
|
'%(what)s is deprecated as of %(as_of)s and may be '
|
||||||
|
'removed in %(remove_in)s. It will not be superseded.')
|
||||||
|
|
||||||
|
_deprecated_msg_with_alternative_no_removal = _(
|
||||||
|
'%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s.')
|
||||||
|
|
||||||
|
_deprecated_msg_with_no_alternative_no_removal = _(
|
||||||
|
'%(what)s is deprecated as of %(as_of)s. It will not be superseded.')
|
||||||
|
|
||||||
|
def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None):
|
||||||
|
"""Initialize decorator
|
||||||
|
|
||||||
|
:param as_of: the release deprecating the callable. Constants
|
||||||
|
are define in this class for convenience.
|
||||||
|
:param in_favor_of: the replacement for the callable (optional)
|
||||||
|
:param remove_in: an integer specifying how many releases to wait
|
||||||
|
before removing (default: 2)
|
||||||
|
:param what: name of the thing being deprecated (default: the
|
||||||
|
callable's name)
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.as_of = as_of
|
||||||
|
self.in_favor_of = in_favor_of
|
||||||
|
self.remove_in = remove_in
|
||||||
|
self.what = what
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
if not self.what:
|
||||||
|
self.what = func.__name__ + '()'
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
msg, details = self._build_message()
|
||||||
|
LOG.deprecated(msg, details)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
def _get_safe_to_remove_release(self, release):
|
||||||
|
# TODO(dstanek): this method will have to be reimplemented once
|
||||||
|
# when we get to the X release because once we get to the Y
|
||||||
|
# release, what is Y+2?
|
||||||
|
new_release = chr(ord(release) + self.remove_in)
|
||||||
|
if new_release in self._RELEASES:
|
||||||
|
return self._RELEASES[new_release]
|
||||||
|
else:
|
||||||
|
return new_release
|
||||||
|
|
||||||
|
def _build_message(self):
|
||||||
|
details = dict(what=self.what,
|
||||||
|
as_of=self._RELEASES[self.as_of],
|
||||||
|
remove_in=self._get_safe_to_remove_release(self.as_of))
|
||||||
|
|
||||||
|
if self.in_favor_of:
|
||||||
|
details['in_favor_of'] = self.in_favor_of
|
||||||
|
if self.remove_in > 0:
|
||||||
|
msg = self._deprecated_msg_with_alternative
|
||||||
|
else:
|
||||||
|
# There are no plans to remove this function, but it is
|
||||||
|
# now deprecated.
|
||||||
|
msg = self._deprecated_msg_with_alternative_no_removal
|
||||||
|
else:
|
||||||
|
if self.remove_in > 0:
|
||||||
|
msg = self._deprecated_msg_no_alternative
|
||||||
|
else:
|
||||||
|
# There are no plans to remove this function, but it is
|
||||||
|
# now deprecated.
|
||||||
|
msg = self._deprecated_msg_with_no_alternative_no_removal
|
||||||
|
return msg, details
|
||||||
|
|
||||||
|
|
||||||
def is_compatible(requested_version, current_version, same_major=True):
|
def is_compatible(requested_version, current_version, same_major=True):
|
||||||
"""Determine whether `requested_version` is satisfied by
|
"""Determine whether `requested_version` is satisfied by
|
||||||
|
@ -16,6 +16,10 @@ TEMPDIR=`mktemp -d /tmp/${PROJECT_NAME}.XXXXXX`
|
|||||||
trap "rm -rf $TEMPDIR" EXIT
|
trap "rm -rf $TEMPDIR" EXIT
|
||||||
|
|
||||||
tools/config/generate_sample.sh -b ./ -p ${PROJECT_NAME} -o ${TEMPDIR}
|
tools/config/generate_sample.sh -b ./ -p ${PROJECT_NAME} -o ${TEMPDIR}
|
||||||
|
if [ $? != 0 ]
|
||||||
|
then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! diff -u ${TEMPDIR}/${CFGFILE_NAME} ${CFGFILE}
|
if ! diff -u ${TEMPDIR}/${CFGFILE_NAME} ${CFGFILE}
|
||||||
then
|
then
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Generate sample configuration for your project.
|
||||||
|
#
|
||||||
|
# Aside from the command line flags, it also respects a config file which
|
||||||
|
# should be named oslo.config.generator.rc and be placed in the same directory.
|
||||||
|
#
|
||||||
|
# You can then export the following variables:
|
||||||
|
# IRONIC_CONFIG_GENERATOR_EXTRA_MODULES: list of modules to interrogate for options.
|
||||||
|
# IRONIC_CONFIG_GENERATOR_EXTRA_LIBRARIES: list of libraries to discover.
|
||||||
|
# IRONIC_CONFIG_GENERATOR_EXCLUDED_FILES: list of files to remove from automatic listing.
|
||||||
|
|
||||||
print_hint() {
|
print_hint() {
|
||||||
echo "Try \`${0##*/} --help' for more information." >&2
|
echo "Try \`${0##*/} --help' for more information." >&2
|
||||||
}
|
}
|
||||||
@ -65,7 +75,7 @@ then
|
|||||||
BASEDIR=$(cd "$BASEDIR" && pwd)
|
BASEDIR=$(cd "$BASEDIR" && pwd)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PACKAGENAME=${PACKAGENAME:-${BASEDIR##*/}}
|
PACKAGENAME=${PACKAGENAME:-$(python setup.py --name)}
|
||||||
TARGETDIR=$BASEDIR/$PACKAGENAME
|
TARGETDIR=$BASEDIR/$PACKAGENAME
|
||||||
if ! [ -d $TARGETDIR ]
|
if ! [ -d $TARGETDIR ]
|
||||||
then
|
then
|
||||||
@ -95,6 +105,10 @@ then
|
|||||||
source "$RC_FILE"
|
source "$RC_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
for filename in ${IRONIC_CONFIG_GENERATOR_EXCLUDED_FILES}; do
|
||||||
|
FILES="${FILES[@]/$filename/}"
|
||||||
|
done
|
||||||
|
|
||||||
for mod in ${IRONIC_CONFIG_GENERATOR_EXTRA_MODULES}; do
|
for mod in ${IRONIC_CONFIG_GENERATOR_EXTRA_MODULES}; do
|
||||||
MODULES="$MODULES -m $mod"
|
MODULES="$MODULES -m $mod"
|
||||||
done
|
done
|
||||||
@ -111,6 +125,11 @@ DEFAULT_MODULEPATH=ironic.openstack.common.config.generator
|
|||||||
MODULEPATH=${MODULEPATH:-$DEFAULT_MODULEPATH}
|
MODULEPATH=${MODULEPATH:-$DEFAULT_MODULEPATH}
|
||||||
OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample
|
OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample
|
||||||
python -m $MODULEPATH $MODULES $LIBRARIES $FILES > $OUTPUTFILE
|
python -m $MODULEPATH $MODULES $LIBRARIES $FILES > $OUTPUTFILE
|
||||||
|
if [ $? != 0 ]
|
||||||
|
then
|
||||||
|
echo "Can not generate $OUTPUTFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Hook to allow projects to append custom config file snippets
|
# Hook to allow projects to append custom config file snippets
|
||||||
CONCAT_FILES=$(ls $BASEDIR/tools/config/*.conf.sample 2>/dev/null)
|
CONCAT_FILES=$(ls $BASEDIR/tools/config/*.conf.sample 2>/dev/null)
|
||||||
|
@ -112,12 +112,12 @@ class InstallVenv(object):
|
|||||||
print('Installing dependencies with pip (this can take a while)...')
|
print('Installing dependencies with pip (this can take a while)...')
|
||||||
|
|
||||||
# First things first, make sure our venv has the latest pip and
|
# First things first, make sure our venv has the latest pip and
|
||||||
# setuptools.
|
# setuptools and pbr
|
||||||
self.pip_install('pip>=1.3')
|
self.pip_install('pip>=1.4')
|
||||||
self.pip_install('setuptools')
|
self.pip_install('setuptools')
|
||||||
|
self.pip_install('pbr')
|
||||||
|
|
||||||
self.pip_install('-r', self.requirements)
|
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
|
||||||
self.pip_install('-r', self.test_requirements)
|
|
||||||
|
|
||||||
def parse_args(self, argv):
|
def parse_args(self, argv):
|
||||||
"""Parses command-line arguments."""
|
"""Parses command-line arguments."""
|
||||||
@ -125,7 +125,7 @@ class InstallVenv(object):
|
|||||||
parser.add_option('-n', '--no-site-packages',
|
parser.add_option('-n', '--no-site-packages',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="Do not inherit packages from global Python "
|
help="Do not inherit packages from global Python "
|
||||||
"install")
|
"install.")
|
||||||
return parser.parse_args(argv[1:])[0]
|
return parser.parse_args(argv[1:])[0]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user