unused stuff have been removed
Change-Id: I413f23ad0882b918283334395efa525285d3d440
This commit is contained in:
parent
b9e45b1eee
commit
e575619368
|
@ -1,241 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2014 Yahoo! Inc. 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.
|
||||
"""State machine modelling, copied from TaskFlow project.
|
||||
|
||||
This work will be turned into a library.
|
||||
See https://github.com/harlowja/automaton
|
||||
|
||||
This is being used in the implementation of:
|
||||
http://specs.openstack.org/openstack/iotronic-specs/specs/kilo/new-iotronic-state-machine.html
|
||||
"""
|
||||
|
||||
from collections import OrderedDict # noqa
|
||||
|
||||
import six
|
||||
|
||||
from iotronic.common import exception as excp
|
||||
from iotronic.common.i18n import _
|
||||
|
||||
|
||||
class _Jump(object):
|
||||
"""A FSM transition tracks this data while jumping."""
|
||||
|
||||
def __init__(self, name, on_enter, on_exit):
|
||||
self.name = name
|
||||
self.on_enter = on_enter
|
||||
self.on_exit = on_exit
|
||||
|
||||
|
||||
class FSM(object):
|
||||
"""A finite state machine.
|
||||
|
||||
This class models a state machine, and expects an outside caller to
|
||||
manually trigger the state changes one at a time by invoking process_event
|
||||
"""
|
||||
|
||||
def __init__(self, start_state=None):
|
||||
self._transitions = {}
|
||||
self._states = OrderedDict()
|
||||
self._start_state = start_state
|
||||
self._target_state = None
|
||||
# Note that _current is a _Jump instance
|
||||
self._current = None
|
||||
|
||||
@property
|
||||
def start_state(self):
|
||||
return self._start_state
|
||||
|
||||
@property
|
||||
def current_state(self):
|
||||
if self._current is not None:
|
||||
return self._current.name
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_state(self):
|
||||
return self._target_state
|
||||
|
||||
@property
|
||||
def terminated(self):
|
||||
"""Returns whether the state machine is in a terminal state."""
|
||||
if self._current is None:
|
||||
return False
|
||||
return self._states[self._current.name]['terminal']
|
||||
|
||||
def add_state(self, state, on_enter=None, on_exit=None,
|
||||
target=None, terminal=None, stable=False):
|
||||
"""Adds a given state to the state machine.
|
||||
|
||||
The on_enter and on_exit callbacks, if provided will be expected to
|
||||
take two positional parameters, these being the state being exited (for
|
||||
on_exit) or the state being entered (for on_enter) and a second
|
||||
parameter which is the event that is being processed that caused the
|
||||
state transition.
|
||||
|
||||
:param stable: Use this to specify that this state is a stable/passive
|
||||
state. A state must have been previously defined as
|
||||
'stable' before it can be used as a 'target'
|
||||
:param target: The target state for 'state' to go to. Before a state
|
||||
can be used as a target it must have been previously
|
||||
added and specified as 'stable'
|
||||
"""
|
||||
if state in self._states:
|
||||
raise excp.Duplicate(_("State '%s' already defined") % state)
|
||||
if on_enter is not None:
|
||||
if not six.callable(on_enter):
|
||||
raise ValueError(_("On enter callback must be callable"))
|
||||
if on_exit is not None:
|
||||
if not six.callable(on_exit):
|
||||
raise ValueError(_("On exit callback must be callable"))
|
||||
if target is not None and target not in self._states:
|
||||
raise excp.InvalidState(_("Target state '%s' does not exist")
|
||||
% target)
|
||||
if target is not None and not self._states[target]['stable']:
|
||||
raise excp.InvalidState(
|
||||
_("Target state '%s' is not a 'stable' state") % target)
|
||||
|
||||
self._states[state] = {
|
||||
'terminal': bool(terminal),
|
||||
'reactions': {},
|
||||
'on_enter': on_enter,
|
||||
'on_exit': on_exit,
|
||||
'target': target,
|
||||
'stable': stable,
|
||||
}
|
||||
self._transitions[state] = OrderedDict()
|
||||
|
||||
def add_transition(self, start, end, event):
|
||||
"""Adds an allowed transition from start -> end for the given event."""
|
||||
if start not in self._states:
|
||||
raise excp.NotFound(
|
||||
_("Can not add a transition on event '%(event)s' that "
|
||||
"starts in a undefined state '%(state)s'")
|
||||
% {'event': event, 'state': start})
|
||||
if end not in self._states:
|
||||
raise excp.NotFound(
|
||||
_("Can not add a transition on event '%(event)s' that "
|
||||
"ends in a undefined state '%(state)s'")
|
||||
% {'event': event, 'state': end})
|
||||
self._transitions[start][event] = _Jump(end,
|
||||
self._states[end]['on_enter'],
|
||||
self._states[start]['on_exit'])
|
||||
|
||||
def process_event(self, event):
|
||||
"""Trigger a state change in response to the provided event."""
|
||||
current = self._current
|
||||
if current is None:
|
||||
raise excp.InvalidState(_("Can only process events after"
|
||||
" being initialized (not before)"))
|
||||
if self._states[current.name]['terminal']:
|
||||
raise excp.InvalidState(
|
||||
_("Can not transition from terminal "
|
||||
"state '%(state)s' on event '%(event)s'")
|
||||
% {'state': current.name, 'event': event})
|
||||
if event not in self._transitions[current.name]:
|
||||
raise excp.InvalidState(
|
||||
_("Can not transition from state '%(state)s' on "
|
||||
"event '%(event)s' (no defined transition)")
|
||||
% {'state': current.name, 'event': event})
|
||||
replacement = self._transitions[current.name][event]
|
||||
if current.on_exit is not None:
|
||||
current.on_exit(current.name, event)
|
||||
if replacement.on_enter is not None:
|
||||
replacement.on_enter(replacement.name, event)
|
||||
self._current = replacement
|
||||
|
||||
# clear _target if we've reached it
|
||||
if (self._target_state is not None and
|
||||
self._target_state == replacement.name):
|
||||
self._target_state = None
|
||||
# if new state has a different target, update the target
|
||||
if self._states[replacement.name]['target'] is not None:
|
||||
self._target_state = self._states[replacement.name]['target']
|
||||
|
||||
def is_valid_event(self, event):
|
||||
"""Check whether the event is actionable in the current state."""
|
||||
current = self._current
|
||||
if current is None:
|
||||
return False
|
||||
if self._states[current.name]['terminal']:
|
||||
return False
|
||||
if event not in self._transitions[current.name]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def initialize(self, state=None):
|
||||
"""Sets up the state machine.
|
||||
|
||||
sets the current state to the specified state, or start_state
|
||||
if no state was specified..
|
||||
"""
|
||||
if state is None:
|
||||
state = self._start_state
|
||||
if state not in self._states:
|
||||
raise excp.NotFound(_("Can not start from an undefined"
|
||||
" state '%s'") % (state))
|
||||
if self._states[state]['terminal']:
|
||||
raise excp.InvalidState(_("Can not start from a terminal"
|
||||
" state '%s'") % (state))
|
||||
self._current = _Jump(state, None, None)
|
||||
self._target_state = self._states[state]['target']
|
||||
|
||||
def copy(self, shallow=False):
|
||||
"""Copies the current state machine (shallow or deep).
|
||||
|
||||
NOTE(harlowja): the copy will be left in an *uninitialized* state.
|
||||
|
||||
NOTE(harlowja): when a shallow copy is requested the copy will share
|
||||
the same transition table and state table as the
|
||||
source; this can be advantageous if you have a machine
|
||||
and transitions + states that is defined somewhere
|
||||
and want to use copies to run with (the copies have
|
||||
the current state that is different between machines).
|
||||
"""
|
||||
c = FSM(self.start_state)
|
||||
if not shallow:
|
||||
for state, data in six.iteritems(self._states):
|
||||
copied_data = data.copy()
|
||||
copied_data['reactions'] = copied_data['reactions'].copy()
|
||||
c._states[state] = copied_data
|
||||
for state, data in six.iteritems(self._transitions):
|
||||
c._transitions[state] = data.copy()
|
||||
else:
|
||||
c._transitions = self._transitions
|
||||
c._states = self._states
|
||||
return c
|
||||
|
||||
def __contains__(self, state):
|
||||
"""Returns if this state exists in the machines known states."""
|
||||
return state in self._states
|
||||
|
||||
@property
|
||||
def states(self):
|
||||
"""Returns a list of the state names."""
|
||||
return list(six.iterkeys(self._states))
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterates over (start, event, end) transition tuples."""
|
||||
for state in six.iterkeys(self._states):
|
||||
for event, target in six.iteritems(self._transitions[state]):
|
||||
yield (state, event, target.name)
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Returns how many events exist."""
|
||||
c = 0
|
||||
for state in six.iterkeys(self._states):
|
||||
c += len(self._transitions[state])
|
||||
return c
|
|
@ -1,287 +0,0 @@
|
|||
# Copyright 2010 OpenStack Foundation
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from glanceclient import client
|
||||
from glanceclient import exc as glance_exc
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import sendfile
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from iotronic.common import exception
|
||||
from iotronic.common.glance_service import service_utils
|
||||
from iotronic.common.i18n import _LE
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _translate_image_exception(image_id, exc_value):
|
||||
if isinstance(exc_value, (glance_exc.Forbidden,
|
||||
glance_exc.Unauthorized)):
|
||||
return exception.ImageNotAuthorized(image_id=image_id)
|
||||
if isinstance(exc_value, glance_exc.NotFound):
|
||||
return exception.ImageNotFound(image_id=image_id)
|
||||
if isinstance(exc_value, glance_exc.BadRequest):
|
||||
return exception.Invalid(exc_value)
|
||||
return exc_value
|
||||
|
||||
|
||||
def _translate_plain_exception(exc_value):
|
||||
if isinstance(exc_value, (glance_exc.Forbidden,
|
||||
glance_exc.Unauthorized)):
|
||||
return exception.NotAuthorized(exc_value)
|
||||
if isinstance(exc_value, glance_exc.NotFound):
|
||||
return exception.NotFound(exc_value)
|
||||
if isinstance(exc_value, glance_exc.BadRequest):
|
||||
return exception.Invalid(exc_value)
|
||||
return exc_value
|
||||
|
||||
|
||||
def check_image_service(func):
|
||||
"""Creates a glance client if doesn't exists and calls the function."""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""Wrapper around methods calls.
|
||||
|
||||
:param image_href: href that describes the location of an image
|
||||
"""
|
||||
|
||||
if self.client:
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
image_href = kwargs.get('image_href')
|
||||
(image_id, self.glance_host,
|
||||
self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href)
|
||||
|
||||
if use_ssl:
|
||||
scheme = 'https'
|
||||
else:
|
||||
scheme = 'http'
|
||||
params = {}
|
||||
params['insecure'] = CONF.glance.glance_api_insecure
|
||||
if CONF.glance.auth_strategy == 'keystone':
|
||||
params['token'] = self.context.auth_token
|
||||
endpoint = '%s://%s:%s' % (scheme, self.glance_host, self.glance_port)
|
||||
self.client = client.Client(self.version,
|
||||
endpoint, **params)
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class BaseImageService(object):
|
||||
|
||||
def __init__(self, client=None, version=1, context=None):
|
||||
self.client = client
|
||||
self.version = version
|
||||
self.context = context
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
"""Call a glance client method.
|
||||
|
||||
If we get a connection error,
|
||||
retry the request according to CONF.glance_num_retries.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param version: The requested API version.v
|
||||
:param method: The method requested to be called.
|
||||
:param args: A list of positional arguments for the method called
|
||||
:param kwargs: A dict of keyword arguments for the method called
|
||||
|
||||
:raises: GlanceConnectionFailed
|
||||
"""
|
||||
retry_excs = (glance_exc.ServiceUnavailable,
|
||||
glance_exc.InvalidEndpoint,
|
||||
glance_exc.CommunicationError)
|
||||
image_excs = (glance_exc.Forbidden,
|
||||
glance_exc.Unauthorized,
|
||||
glance_exc.NotFound,
|
||||
glance_exc.BadRequest)
|
||||
num_attempts = 1 + CONF.glance.glance_num_retries
|
||||
|
||||
for attempt in range(1, num_attempts + 1):
|
||||
try:
|
||||
return getattr(self.client.images, method)(*args, **kwargs)
|
||||
except retry_excs as e:
|
||||
host = self.glance_host
|
||||
port = self.glance_port
|
||||
error_msg = _LE("Error contacting glance server "
|
||||
"'%(host)s:%(port)s' for '%(method)s', attempt"
|
||||
" %(attempt)s of %(num_attempts)s failed.")
|
||||
LOG.exception(error_msg, {'host': host,
|
||||
'port': port,
|
||||
'num_attempts': num_attempts,
|
||||
'attempt': attempt,
|
||||
'method': method})
|
||||
if attempt == num_attempts:
|
||||
raise exception.GlanceConnectionFailed(host=host,
|
||||
port=port,
|
||||
reason=str(e))
|
||||
time.sleep(1)
|
||||
except image_excs as e:
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
if method == 'list':
|
||||
new_exc = _translate_plain_exception(
|
||||
exc_value)
|
||||
else:
|
||||
new_exc = _translate_image_exception(
|
||||
args[0], exc_value)
|
||||
six.reraise(type(new_exc), new_exc, exc_trace)
|
||||
|
||||
@check_image_service
|
||||
def _detail(self, method='list', **kwargs):
|
||||
"""Calls out to Glance for a list of detailed image information.
|
||||
|
||||
:returns: A list of dicts containing image metadata.
|
||||
"""
|
||||
LOG.debug("Getting a full list of images metadata from glance.")
|
||||
params = service_utils.extract_query_params(kwargs, self.version)
|
||||
|
||||
images = self.call(method, **params)
|
||||
|
||||
_images = []
|
||||
for image in images:
|
||||
if service_utils.is_image_available(self.context, image):
|
||||
_images.append(service_utils.translate_from_glance(image))
|
||||
|
||||
return _images
|
||||
|
||||
@check_image_service
|
||||
def _show(self, image_href, method='get'):
|
||||
"""Returns a dict with image data for the given opaque image id.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:returns: A dict containing image metadata.
|
||||
|
||||
:raises: ImageNotFound
|
||||
"""
|
||||
LOG.debug("Getting image metadata from glance. Image: %s"
|
||||
% image_href)
|
||||
(image_id, self.glance_host,
|
||||
self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href)
|
||||
|
||||
image = self.call(method, image_id)
|
||||
|
||||
if not service_utils.is_image_available(self.context, image):
|
||||
raise exception.ImageNotFound(image_id=image_id)
|
||||
|
||||
base_image_meta = service_utils.translate_from_glance(image)
|
||||
return base_image_meta
|
||||
|
||||
@check_image_service
|
||||
def _download(self, image_id, data=None, method='data'):
|
||||
"""Calls out to Glance for data and writes data.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:param data: (Optional) File object to write data to.
|
||||
"""
|
||||
(image_id, self.glance_host,
|
||||
self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
|
||||
|
||||
if (self.version == 2 and
|
||||
'file' in CONF.glance.allowed_direct_url_schemes):
|
||||
|
||||
location = self._get_location(image_id)
|
||||
url = urlparse.urlparse(location)
|
||||
if url.scheme == "file":
|
||||
with open(url.path, "r") as f:
|
||||
filesize = os.path.getsize(f.name)
|
||||
sendfile.sendfile(data.fileno(), f.fileno(), 0, filesize)
|
||||
return
|
||||
|
||||
image_chunks = self.call(method, image_id)
|
||||
|
||||
if data is None:
|
||||
return image_chunks
|
||||
else:
|
||||
for chunk in image_chunks:
|
||||
data.write(chunk)
|
||||
|
||||
@check_image_service
|
||||
def _create(self, image_meta, data=None, method='create'):
|
||||
"""Store the image data and return the new image object.
|
||||
|
||||
:param image_meta: A dict containing image metadata
|
||||
:param data: (Optional) File object to create image from.
|
||||
:returns: dict -- New created image metadata
|
||||
"""
|
||||
sent_service_image_meta = service_utils.translate_to_glance(image_meta)
|
||||
|
||||
# TODO(ghe): Allow copy-from or location headers Bug #1199532
|
||||
|
||||
if data:
|
||||
sent_service_image_meta['data'] = data
|
||||
|
||||
recv_service_image_meta = self.call(method, **sent_service_image_meta)
|
||||
|
||||
return service_utils.translate_from_glance(recv_service_image_meta)
|
||||
|
||||
@check_image_service
|
||||
def _update(self, image_id, image_meta, data=None, method='update',
|
||||
purge_props=False):
|
||||
"""Modify the given image with the new data.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:param data: (Optional) File object to update data from.
|
||||
:param purge_props: (Optional=False) Purge existing properties.
|
||||
:returns: dict -- New created image metadata
|
||||
"""
|
||||
(image_id, self.glance_host,
|
||||
self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
|
||||
if image_meta:
|
||||
image_meta = service_utils.translate_to_glance(image_meta)
|
||||
else:
|
||||
image_meta = {}
|
||||
if self.version == 1:
|
||||
image_meta['purge_props'] = purge_props
|
||||
if data:
|
||||
image_meta['data'] = data
|
||||
|
||||
# NOTE(bcwaldon): id is not an editable field, but it is likely to be
|
||||
# passed in by calling code. Let's be nice and ignore it.
|
||||
image_meta.pop('id', None)
|
||||
|
||||
image_meta = self.call(method, image_id, **image_meta)
|
||||
|
||||
if self.version == 2 and data:
|
||||
self.call('upload', image_id, data)
|
||||
image_meta = self._show(image_id)
|
||||
|
||||
return image_meta
|
||||
|
||||
@check_image_service
|
||||
def _delete(self, image_id, method='delete'):
|
||||
"""Delete the given image.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
|
||||
:raises: ImageNotFound if the image does not exist.
|
||||
:raises: NotAuthorized if the user is not an owner.
|
||||
:raises: ImageNotAuthorized if the user is not authorized.
|
||||
|
||||
"""
|
||||
(image_id, glance_host,
|
||||
glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
|
||||
|
||||
self.call(method, image_id)
|
|
@ -1,81 +0,0 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ImageService(object):
|
||||
"""Provides storage and retrieval of disk image objects within Glance."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def detail(self):
|
||||
"""Calls out to Glance for a list of detailed image information."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def show(self, image_id):
|
||||
"""Returns a dict with image data for the given opaque image id.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:returns: A dict containing image metadata.
|
||||
|
||||
:raises: ImageNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, image_id, data=None):
|
||||
"""Calls out to Glance for data and writes data.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:param data: (Optional) File object to write data to.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create(self, image_meta, data=None):
|
||||
"""Store the image data and return the new image object.
|
||||
|
||||
:param image_meta: A dict containing image metadata
|
||||
:param data: (Optional) File object to create image from.
|
||||
:returns: dict -- New created image metadata
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update(self, image_id,
|
||||
image_meta, data=None, purge_props=False):
|
||||
"""Modify the given image with the new data.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
:param data: (Optional) File object to update data from.
|
||||
:param purge_props: (Optional=True) Purge existing properties.
|
||||
:returns: dict -- New created image metadata
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete(self, image_id):
|
||||
"""Delete the given image.
|
||||
|
||||
:param image_id: The opaque image identifier.
|
||||
|
||||
:raises: ImageNotFound if the image does not exist.
|
||||
:raises: NotAuthorized if the user is not an owner.
|
||||
:raises: ImageNotAuthorized if the user is not authorized.
|
||||
|
||||
"""
|
|
@ -1,245 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from iotronic.common import exception
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
_GLANCE_API_SERVER = None
|
||||
""" iterator that cycles (indefinitely) over glance API servers. """
|
||||
|
||||
|
||||
def generate_glance_url():
|
||||
"""Generate the URL to glance."""
|
||||
return "%s://%s:%d" % (CONF.glance.glance_protocol,
|
||||
CONF.glance.glance_host,
|
||||
CONF.glance.glance_port)
|
||||
|
||||
|
||||
def generate_image_url(image_ref):
|
||||
"""Generate an image URL from an image_ref."""
|
||||
return "%s/images/%s" % (generate_glance_url(), image_ref)
|
||||
|
||||
|
||||
def _extract_attributes(image):
|
||||
IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner',
|
||||
'container_format', 'checksum', 'id',
|
||||
'name', 'created_at', 'updated_at',
|
||||
'deleted_at', 'deleted', 'status',
|
||||
'min_disk', 'min_ram', 'is_public']
|
||||
|
||||
IMAGE_ATTRIBUTES_V2 = ['tags', 'visibility', 'protected',
|
||||
'file', 'schema']
|
||||
|
||||
output = {}
|
||||
for attr in IMAGE_ATTRIBUTES:
|
||||
output[attr] = getattr(image, attr, None)
|
||||
|
||||
output['properties'] = getattr(image, 'properties', {})
|
||||
|
||||
if hasattr(image, 'schema') and 'v2' in image['schema']:
|
||||
IMAGE_ATTRIBUTES = IMAGE_ATTRIBUTES + IMAGE_ATTRIBUTES_V2
|
||||
for attr in IMAGE_ATTRIBUTES_V2:
|
||||
output[attr] = getattr(image, attr, None)
|
||||
output['schema'] = image['schema']
|
||||
|
||||
for image_property in set(image.keys()) - set(IMAGE_ATTRIBUTES):
|
||||
output['properties'][image_property] = image[image_property]
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _convert_timestamps_to_datetimes(image_meta):
|
||||
"""Returns image with timestamp fields converted to datetime objects."""
|
||||
for attr in ['created_at', 'updated_at', 'deleted_at']:
|
||||
if image_meta.get(attr):
|
||||
image_meta[attr] = timeutils.parse_isotime(image_meta[attr])
|
||||
return image_meta
|
||||
|
||||
_CONVERT_PROPS = ('block_device_mapping', 'mappings')
|
||||
|
||||
|
||||
def _convert(metadata, method):
|
||||
metadata = copy.deepcopy(metadata)
|
||||
properties = metadata.get('properties')
|
||||
if properties:
|
||||
for attr in _CONVERT_PROPS:
|
||||
if attr in properties:
|
||||
prop = properties[attr]
|
||||
if method == 'from':
|
||||
if isinstance(prop, six.string_types):
|
||||
properties[attr] = jsonutils.loads(prop)
|
||||
if method == 'to':
|
||||
if not isinstance(prop, six.string_types):
|
||||
properties[attr] = jsonutils.dumps(prop)
|
||||
return metadata
|
||||
|
||||
|
||||
def _remove_read_only(image_meta):
|
||||
IMAGE_ATTRIBUTES = ['status', 'updated_at', 'created_at', 'deleted_at']
|
||||
output = copy.deepcopy(image_meta)
|
||||
for attr in IMAGE_ATTRIBUTES:
|
||||
if attr in output:
|
||||
del output[attr]
|
||||
return output
|
||||
|
||||
|
||||
def _get_api_server_iterator():
|
||||
"""Return iterator over shuffled API servers.
|
||||
|
||||
Shuffle a list of CONF.glance.glance_api_servers and return an iterator
|
||||
that will cycle through the list, looping around to the beginning if
|
||||
necessary.
|
||||
|
||||
If CONF.glance.glance_api_servers isn't set, we fall back to using this
|
||||
as the server: CONF.glance.glance_host:CONF.glance.glance_port.
|
||||
|
||||
:returns: iterator that cycles (indefinitely) over shuffled glance API
|
||||
servers. The iterator returns tuples of (host, port, use_ssl).
|
||||
"""
|
||||
api_servers = []
|
||||
|
||||
configured_servers = (CONF.glance.glance_api_servers or
|
||||
['%s:%s' % (CONF.glance.glance_host,
|
||||
CONF.glance.glance_port)])
|
||||
for api_server in configured_servers:
|
||||
if '//' not in api_server:
|
||||
api_server = '%s://%s' % (CONF.glance.glance_protocol, api_server)
|
||||
url = urlparse.urlparse(api_server)
|
||||
port = url.port or 80
|
||||
host = url.netloc.split(':', 1)[0]
|
||||
use_ssl = (url.scheme == 'https')
|
||||
api_servers.append((host, port, use_ssl))
|
||||
random.shuffle(api_servers)
|
||||
return itertools.cycle(api_servers)
|
||||
|
||||
|
||||
def _get_api_server():
|
||||
"""Return a Glance API server.
|
||||
|
||||
:returns: for an API server, the tuple (host-or-IP, port, use_ssl), where
|
||||
use_ssl is True to use the 'https' scheme, and False to use 'http'.
|
||||
"""
|
||||
global _GLANCE_API_SERVER
|
||||
|
||||
if not _GLANCE_API_SERVER:
|
||||
_GLANCE_API_SERVER = _get_api_server_iterator()
|
||||
return six.next(_GLANCE_API_SERVER)
|
||||
|
||||
|
||||
def parse_image_ref(image_href):
|
||||
"""Parse an image href into composite parts.
|
||||
|
||||
:param image_href: href of an image
|
||||
:returns: a tuple of the form (image_id, host, port, use_ssl)
|
||||
|
||||
:raises ValueError
|
||||
"""
|
||||
if '/' not in str(image_href):
|
||||
image_id = image_href
|
||||
(glance_host, glance_port, use_ssl) = _get_api_server()
|
||||
return (image_id, glance_host, glance_port, use_ssl)
|
||||
else:
|
||||
try:
|
||||
url = urlparse.urlparse(image_href)
|
||||
if url.scheme == 'glance':
|
||||
(glance_host, glance_port, use_ssl) = _get_api_server()
|
||||
image_id = image_href.split('/')[-1]
|
||||
else:
|
||||
glance_port = url.port or 80
|
||||
glance_host = url.netloc.split(':', 1)[0]
|
||||
image_id = url.path.split('/')[-1]
|
||||
use_ssl = (url.scheme == 'https')
|
||||
return (image_id, glance_host, glance_port, use_ssl)
|
||||
except ValueError:
|
||||
raise exception.InvalidImageRef(image_href=image_href)
|
||||
|
||||
|
||||
def extract_query_params(params, version):
|
||||
_params = {}
|
||||
accepted_params = ('filters', 'marker', 'limit',
|
||||
'sort_key', 'sort_dir')
|
||||
for param in accepted_params:
|
||||
if params.get(param):
|
||||
_params[param] = params.get(param)
|
||||
# ensure filters is a dict
|
||||
_params.setdefault('filters', {})
|
||||
|
||||
# NOTE(vish): don't filter out private images
|
||||
# NOTE(ghe): in v2, not passing any visibility doesn't filter prvate images
|
||||
if version == 1:
|
||||
_params['filters'].setdefault('is_public', 'none')
|
||||
|
||||
return _params
|
||||
|
||||
|
||||
def translate_to_glance(image_meta):
|
||||
image_meta = _convert(image_meta, 'to')
|
||||
image_meta = _remove_read_only(image_meta)
|
||||
return image_meta
|
||||
|
||||
|
||||
def translate_from_glance(image):
|
||||
image_meta = _extract_attributes(image)
|
||||
image_meta = _convert_timestamps_to_datetimes(image_meta)
|
||||
image_meta = _convert(image_meta, 'from')
|
||||
return image_meta
|
||||
|
||||
|
||||
def is_image_available(context, image):
|
||||
"""Check image availability.
|
||||
|
||||
This check is needed in case Nova and Glance are deployed
|
||||
without authentication turned on.
|
||||
"""
|
||||
# The presence of an auth token implies this is an authenticated
|
||||
# request and we need not handle the noauth use-case.
|
||||
if hasattr(context, 'auth_token') and context.auth_token:
|
||||
return True
|
||||
if image.is_public or context.is_admin:
|
||||
return True
|
||||
properties = image.properties
|
||||
if context.project_id and ('owner_id' in properties):
|
||||
return str(properties['owner_id']) == str(context.project_id)
|
||||
|
||||
if context.project_id and ('project_id' in properties):
|
||||
return str(properties['project_id']) == str(context.project_id)
|
||||
|
||||
try:
|
||||
user_id = properties['user_id']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return str(user_id) == str(context.user_id)
|
||||
|
||||
|
||||
def is_glance_image(image_href):
|
||||
if not isinstance(image_href, six.string_types):
|
||||
return False
|
||||
return (image_href.startswith('glance://') or
|
||||
uuidutils.is_uuid_like(image_href))
|
|
@ -1,41 +0,0 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from iotronic.common.glance_service import base_image_service
|
||||
from iotronic.common.glance_service import service
|
||||
|
||||
|
||||
class GlanceImageService(base_image_service.BaseImageService,
|
||||
service.ImageService):
|
||||
|
||||
def detail(self, **kwargs):
|
||||
return self._detail(method='list', **kwargs)
|
||||
|
||||
def show(self, image_id):
|
||||
return self._show(image_id, method='get')
|
||||
|
||||
def download(self, image_id, data=None):
|
||||
return self._download(image_id, method='data', data=data)
|
||||
|
||||
def create(self, image_meta, data=None):
|
||||
return self._create(image_meta, method='create', data=data)
|
||||
|
||||
def update(self, image_id, image_meta, data=None, purge_props=False):
|
||||
return self._update(image_id, image_meta, data=data, method='update',
|
||||
purge_props=purge_props)
|
||||
|
||||
def delete(self, image_id):
|
||||
return self._delete(image_id, method='delete')
|
|
@ -1,229 +0,0 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
from swiftclient import utils as swift_utils
|
||||
|
||||
from iotronic.common import exception as exc
|
||||
from iotronic.common.glance_service import base_image_service
|
||||
from iotronic.common.glance_service import service
|
||||
from iotronic.common.glance_service import service_utils
|
||||
from iotronic.common.i18n import _
|
||||
|
||||
|
||||
glance_opts = [
|
||||
cfg.ListOpt('allowed_direct_url_schemes',
|
||||
default=[],
|
||||
help='A list of URL schemes that can be downloaded directly '
|
||||
'via the direct_url. Currently supported schemes: '
|
||||
'[file].'),
|
||||
# To upload this key to Swift:
|
||||
# swift post -m Temp-Url-Key:correcthorsebatterystaple
|
||||
cfg.StrOpt('swift_temp_url_key',
|
||||
help='The secret token given to Swift to allow temporary URL '
|
||||
'downloads. Required for temporary URLs.',
|
||||
secret=True),
|
||||
cfg.IntOpt('swift_temp_url_duration',
|
||||
min=0,
|
||||
default=1200,
|
||||
help='The length of time in seconds that the temporary URL '
|
||||
'will be valid for. Defaults to 20 minutes. If some '
|
||||
'deploys get a 401 response code when trying to download '
|
||||
'from the temporary URL, try raising this duration.'),
|
||||
cfg.StrOpt('swift_endpoint_url',
|
||||
help='The "endpoint" (scheme, hostname, optional port) for '
|
||||
'the Swift URL of the form '
|
||||
'"endpoint_url/api_version/account/container/object_id". '
|
||||
'Do not include trailing "/". '
|
||||
'For example, use "https://swift.example.com". '
|
||||
'Required for temporary URLs.'),
|
||||
cfg.StrOpt('swift_api_version',
|
||||
default='v1',
|
||||
help='The Swift API version to create a temporary URL for. '
|
||||
'Defaults to "v1". Swift temporary URL format: '
|
||||
'"endpoint_url/api_version/account/container/object_id"'),
|
||||
cfg.StrOpt('swift_account',
|
||||
help='The account that Glance uses to communicate with '
|
||||
'Swift. The format is "AUTH_uuid". "uuid" is the '
|
||||
'UUID for the account configured in the glance-api.conf. '
|
||||
'Required for temporary URLs. For example: '
|
||||
'"AUTH_a422b2-91f3-2f46-74b7-d7c9e8958f5d30". '
|
||||
'Swift temporary URL format: '
|
||||
'"endpoint_url/api_version/account/container/object_id"'),
|
||||
cfg.StrOpt('swift_container',
|
||||
default='glance',
|
||||
help='The Swift container Glance is configured to store its '
|
||||
'images in. Defaults to "glance", which is the default '
|
||||
'in glance-api.conf. '
|
||||
'Swift temporary URL format: '
|
||||
'"endpoint_url/api_version/account/container/object_id"'),
|
||||
cfg.IntOpt('swift_store_multiple_containers_seed',
|
||||
default=0,
|
||||
help='This should match a config by the same name in the '
|
||||
'Glance configuration file. When set to 0, a '
|
||||
'single-tenant store will only use one '
|
||||
'container to store all images. When set to an integer '
|
||||
'value between 1 and 32, a single-tenant store will use '
|
||||
'multiple containers to store images, and this value '
|
||||
'will determine how many containers are created.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(glance_opts, group='glance')
|
||||
|
||||
|
||||
class GlanceImageService(base_image_service.BaseImageService,
|
||||
service.ImageService):
|
||||
|
||||
def detail(self, **kwargs):
|
||||
return self._detail(method='list', **kwargs)
|
||||
|
||||
def show(self, image_id):
|
||||
return self._show(image_id, method='get')
|
||||
|
||||
def download(self, image_id, data=None):
|
||||
return self._download(image_id, method='data', data=data)
|
||||
|
||||
def create(self, image_meta, data=None):
|
||||
image_id = self._create(image_meta, method='create', data=None)['id']
|
||||
return self.update(image_id, None, data)
|
||||
|
||||
def update(self, image_id, image_meta, data=None, purge_props=False):
|
||||
# NOTE(ghe): purge_props not working until bug 1206472 solved
|
||||
return self._update(image_id, image_meta, data, method='update',
|
||||
purge_props=False)
|
||||
|
||||
def delete(self, image_id):
|
||||
return self._delete(image_id, method='delete')
|
||||
|
||||
def swift_temp_url(self, image_info):
|
||||
"""Generate a no-auth Swift temporary URL.
|
||||
|
||||
This function will generate the temporary Swift URL using the image
|
||||
id from Glance and the config options: 'swift_endpoint_url',
|
||||
'swift_api_version', 'swift_account' and 'swift_container'.
|
||||
The temporary URL will be valid for 'swift_temp_url_duration' seconds.
|
||||
This allows Iotronic to download a Glance image without passing around
|
||||
an auth_token.
|
||||
|
||||
:param image_info: The return from a GET request to Glance for a
|
||||
certain image_id. Should be a dictionary, with keys like 'name' and
|
||||
'checksum'. See
|
||||
http://docs.openstack.org/developer/glance/glanceapi.html for
|
||||
examples.
|
||||
:returns: A signed Swift URL from which an image can be downloaded,
|
||||
without authentication.
|
||||
|
||||
:raises: InvalidParameterValue if Swift config options are not set
|
||||
correctly.
|
||||
:raises: MissingParameterValue if a required parameter is not set.
|
||||
:raises: ImageUnacceptable if the image info from Glance does not
|
||||
have a image ID.
|
||||
"""
|
||||
self._validate_temp_url_config()
|
||||
|
||||
if ('id' not in image_info or not
|
||||
uuidutils.is_uuid_like(image_info['id'])):
|
||||
raise exc.ImageUnacceptable(_(
|
||||
'The given image info does not have a valid image id: %s')
|
||||
% image_info)
|
||||
|
||||
url_fragments = {
|
||||
'endpoint_url': CONF.glance.swift_endpoint_url,
|
||||
'api_version': CONF.glance.swift_api_version,
|
||||
'account': CONF.glance.swift_account,
|
||||
'container': self._get_swift_container(image_info['id']),
|
||||
'object_id': image_info['id']
|
||||
}
|
||||
|
||||
template = '/{api_version}/{account}/{container}/{object_id}'
|
||||
url_path = template.format(**url_fragments)
|
||||
path = swift_utils.generate_temp_url(
|
||||
path=url_path,
|
||||
seconds=CONF.glance.swift_temp_url_duration,
|
||||
key=CONF.glance.swift_temp_url_key,
|
||||
method='GET')
|
||||
|
||||
return '{endpoint_url}{url_path}'.format(
|
||||
endpoint_url=url_fragments['endpoint_url'], url_path=path)
|
||||
|
||||
def _validate_temp_url_config(self):
|
||||
"""Validate the required settings for a temporary URL."""
|
||||
if not CONF.glance.swift_temp_url_key:
|
||||
raise exc.MissingParameterValue(_(
|
||||
'Swift temporary URLs require a shared secret to be created. '
|
||||
'You must provide "swift_temp_url_key" as a config option.'))
|
||||
if not CONF.glance.swift_endpoint_url:
|
||||
raise exc.MissingParameterValue(_(
|
||||
'Swift temporary URLs require a Swift endpoint URL. '
|
||||
'You must provide "swift_endpoint_url" as a config option.'))
|
||||
if not CONF.glance.swift_account:
|
||||
raise exc.MissingParameterValue(_(
|
||||
'Swift temporary URLs require a Swift account string. '
|
||||
'You must provide "swift_account" as a config option.'))
|
||||
seed_num_chars = CONF.glance.swift_store_multiple_containers_seed
|
||||
if (seed_num_chars is None or seed_num_chars < 0
|
||||
or seed_num_chars > 32):
|
||||
raise exc.InvalidParameterValue(_(
|
||||
"An integer value between 0 and 32 is required for"
|
||||
" swift_store_multiple_containers_seed."))
|
||||
|
||||
def _get_swift_container(self, image_id):
|
||||
"""Get the Swift container the image is stored in.
|
||||
|
||||
Code based on: https://github.com/openstack/glance_store/blob/3cd690b3
|
||||
7dc9d935445aca0998e8aec34a3e3530/glance_store/
|
||||
_drivers/swift/store.py#L725
|
||||
|
||||
Returns appropriate container name depending upon value of
|
||||
``swift_store_multiple_containers_seed``. In single-container mode,
|
||||
which is a seed value of 0, simply returns ``swift_container``.
|
||||
In multiple-container mode, returns ``swift_container`` as the
|
||||
prefix plus a suffix determined by the multiple container seed
|
||||
|
||||
examples:
|
||||
single-container mode: 'glance'
|
||||
multiple-container mode: 'glance_3a1' for image uuid 3A1xxxxxxx...
|
||||
|
||||
:param image_id: UUID of image
|
||||
:returns: The name of the swift container the image is stored in
|
||||
"""
|
||||
seed_num_chars = CONF.glance.swift_store_multiple_containers_seed
|
||||
|
||||
if seed_num_chars > 0:
|
||||
image_id = str(image_id).lower()
|
||||
|
||||
num_dashes = image_id[:seed_num_chars].count('-')
|
||||
num_chars = seed_num_chars + num_dashes
|
||||
name_suffix = image_id[:num_chars]
|
||||
new_container_name = (CONF.glance.swift_container +
|
||||
'_' + name_suffix)
|
||||
return new_container_name
|
||||
else:
|
||||
return CONF.glance.swift_container
|
||||
|
||||
def _get_location(self, image_id):
|
||||
"""Get storage URL.
|
||||
|
||||
Returns the direct url representing the backend storage location,
|
||||
or None if this attribute is not shown by Glance.
|
||||
"""
|
||||
image_meta = self.call('get', image_id)
|
||||
|
||||
if not service_utils.is_image_available(self.context, image_meta):
|
||||
raise exc.ImageNotFound(image_id=image_id)
|
||||
|
||||
return getattr(image_meta, 'direct_url', None)
|
|
@ -1,8 +0,0 @@
|
|||
set default=0
|
||||
set timeout=5
|
||||
set hidden_timeout_quiet=false
|
||||
|
||||
menuentry "boot_partition" {
|
||||
linuxefi {{ linux }} {{ kernel_params }} --
|
||||
initrdefi {{ initrd }}
|
||||
}
|
|
@ -1,292 +0,0 @@
|
|||
# Copyright 2010 OpenStack Foundation
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import abc
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
import sendfile
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from iotronic.common import exception
|
||||
from iotronic.common.i18n import _
|
||||
from iotronic.common import keystone
|
||||
|
||||
IMAGE_CHUNK_SIZE = 1024 * 1024 # 1mb
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
# Import this opt early so that it is available when registering
|
||||
# glance_opts below.
|
||||
CONF.import_opt('my_ip', 'iotronic.netconf')
|
||||
|
||||
glance_opts = [
|
||||
cfg.StrOpt('glance_host',
|
||||
default='$my_ip',
|
||||
help='Default glance hostname or IP address.'),
|
||||
cfg.IntOpt('glance_port',
|
||||
default=9292,
|
||||
help='Default glance port.'),
|
||||
cfg.StrOpt('glance_protocol',
|
||||
default='http',
|
||||
help='Default protocol to use when connecting to glance. '
|
||||
'Set to https for SSL.'),
|
||||
cfg.ListOpt('glance_api_servers',
|
||||
help='A list of the glance api servers available to iotronic. '
|
||||
'Prefix with https:// for SSL-based glance API servers. '
|
||||
'Format is [hostname|IP]:port.'),
|
||||
cfg.BoolOpt('glance_api_insecure',
|
||||
default=False,
|
||||
help='Allow to perform insecure SSL (https) requests to '
|
||||
'glance.'),
|
||||
cfg.IntOpt('glance_num_retries',
|
||||
default=0,
|
||||
help='Number of retries when downloading an image from '
|
||||
'glance.'),
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='keystone',
|
||||
help='Authentication strategy to use when connecting to '
|
||||
'glance. Only "keystone" and "noauth" are currently '
|
||||
'supported by iotronic.'),
|
||||
]
|
||||
|
||||
CONF.register_opts(glance_opts, group='glance')
|
||||
|
||||
|
||||
def import_versioned_module(version, submodule=None):
|
||||
module = 'iotronic.common.glance_service.v%s' % version
|
||||
if submodule:
|
||||
module = '.'.join((module, submodule))
|
||||
return importutils.try_import(module)
|
||||
|
||||
|
||||
def GlanceImageService(client=None, version=1, context=None):
|
||||
module = import_versioned_module(version, 'image_service')
|
||||
service_class = getattr(module, 'GlanceImageService')
|
||||
if (context is not None
|
||||
and CONF.glance.auth_strategy == 'keystone'
|
||||
and not context.auth_token):
|
||||
context.auth_token = keystone.get_admin_auth_token()
|
||||
return service_class(client, version, context)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseImageService(object):
|
||||
"""Provides retrieval of disk images."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def validate_href(self, image_href):
|
||||
"""Validate image reference.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed.
|
||||
:returns: Information needed to further operate with an image.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, image_href, image_file):
|
||||
"""Downloads image to specified location.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:param image_file: File object to write data to.
|
||||
:raises: exception.ImageRefValidationFailed.
|
||||
:raises: exception.ImageDownloadFailed.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def show(self, image_href):
|
||||
"""Get dictionary of image properties.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed.
|
||||
:returns: dictionary of image properties.
|
||||
"""
|
||||
|
||||
|
||||
class HttpImageService(BaseImageService):
|
||||
"""Provides retrieval of disk images using HTTP."""
|
||||
|
||||
def validate_href(self, image_href):
|
||||
"""Validate HTTP image reference.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed if HEAD request failed or
|
||||
returned response code not equal to 200.
|
||||
:returns: Response to HEAD request.
|
||||
"""
|
||||
try:
|
||||
response = requests.head(image_href)
|
||||
if response.status_code != 200:
|
||||
raise exception.ImageRefValidationFailed(
|
||||
image_href=image_href,
|
||||
reason=_("Got HTTP code %s instead of 200 in response to "
|
||||
"HEAD request.") % response.status_code)
|
||||
except requests.RequestException as e:
|
||||
raise exception.ImageRefValidationFailed(image_href=image_href,
|
||||
reason=e)
|
||||
return response
|
||||
|
||||
def download(self, image_href, image_file):
|
||||
"""Downloads image to specified location.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:param image_file: File object to write data to.
|
||||
:raises: exception.ImageRefValidationFailed if GET request returned
|
||||
response code not equal to 200.
|
||||
:raises: exception.ImageDownloadFailed if:
|
||||
* IOError happened during file write;
|
||||
* GET request failed.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(image_href, stream=True)
|
||||
if response.status_code != 200:
|
||||
raise exception.ImageRefValidationFailed(
|
||||
image_href=image_href,
|
||||
reason=_("Got HTTP code %s instead of 200 in response to "
|
||||
"GET request.") % response.status_code)
|
||||
with response.raw as input_img:
|
||||
shutil.copyfileobj(input_img, image_file, IMAGE_CHUNK_SIZE)
|
||||
except (requests.RequestException, IOError) as e:
|
||||
raise exception.ImageDownloadFailed(image_href=image_href,
|
||||
reason=e)
|
||||
|
||||
def show(self, image_href):
|
||||
"""Get dictionary of image properties.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed if:
|
||||
* HEAD request failed;
|
||||
* HEAD request returned response code not equal to 200;
|
||||
* Content-Length header not found in response to HEAD request.
|
||||
:returns: dictionary of image properties.
|
||||
"""
|
||||
response = self.validate_href(image_href)
|
||||
image_size = response.headers.get('Content-Length')
|
||||
if image_size is None:
|
||||
raise exception.ImageRefValidationFailed(
|
||||
image_href=image_href,
|
||||
reason=_("Cannot determine image size as there is no "
|
||||
"Content-Length header specified in response "
|
||||
"to HEAD request."))
|
||||
return {
|
||||
'size': int(image_size),
|
||||
'properties': {}
|
||||
}
|
||||
|
||||
|
||||
class FileImageService(BaseImageService):
|
||||
"""Provides retrieval of disk images available locally on the conductor."""
|
||||
|
||||
def validate_href(self, image_href):
|
||||
"""Validate local image reference.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed if source image file
|
||||
doesn't exist.
|
||||
:returns: Path to image file if it exists.
|
||||
"""
|
||||
image_path = urlparse.urlparse(image_href).path
|
||||
if not os.path.isfile(image_path):
|
||||
raise exception.ImageRefValidationFailed(
|
||||
image_href=image_href,
|
||||
reason=_("Specified image file not found."))
|
||||
return image_path
|
||||
|
||||
def download(self, image_href, image_file):
|
||||
"""Downloads image to specified location.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:param image_file: File object to write data to.
|
||||
:raises: exception.ImageRefValidationFailed if source image file
|
||||
doesn't exist.
|
||||
:raises: exception.ImageDownloadFailed if exceptions were raised while
|
||||
writing to file or creating hard link.
|
||||
"""
|
||||
source_image_path = self.validate_href(image_href)
|
||||
dest_image_path = image_file.name
|
||||
local_device = os.stat(dest_image_path).st_dev
|
||||
try:
|
||||
# We should have read and write access to source file to create
|
||||
# hard link to it.
|
||||
if (local_device == os.stat(source_image_path).st_dev and
|
||||
os.access(source_image_path, os.R_OK | os.W_OK)):
|
||||
image_file.close()
|
||||
os.remove(dest_image_path)
|
||||
os.link(source_image_path, dest_image_path)
|
||||
else:
|
||||
filesize = os.path.getsize(source_image_path)
|
||||
with open(source_image_path, 'rb') as input_img:
|
||||
sendfile.sendfile(image_file.fileno(), input_img.fileno(),
|
||||
0, filesize)
|
||||
except Exception as e:
|
||||
raise exception.ImageDownloadFailed(image_href=image_href,
|
||||
reason=e)
|
||||
|
||||
def show(self, image_href):
|
||||
"""Get dictionary of image properties.
|
||||
|
||||
:param image_href: Image reference.
|
||||
:raises: exception.ImageRefValidationFailed if image file specified
|
||||
doesn't exist.
|
||||
:returns: dictionary of image properties.
|
||||
"""
|
||||
source_image_path = self.validate_href(image_href)
|
||||
return {
|
||||
'size': os.path.getsize(source_image_path),
|
||||
'properties': {}
|
||||
}
|
||||
|
||||
|
||||
protocol_mapping = {
|
||||
'http': HttpImageService,
|
||||
'https': HttpImageService,
|
||||
'file': FileImageService,
|
||||
'glance': GlanceImageService,
|
||||
}
|
||||
|
||||
|
||||
def get_image_service(image_href, client=None, version=1, context=None):
|
||||
"""Get image service instance to download the image.
|
||||
|
||||
:param image_href: String containing href to get image service for.
|
||||
:param client: Glance client to be used for download, used only if
|
||||
image_href is Glance href.
|
||||
:param version: Version of Glance API to use, used only if image_href is
|
||||
Glance href.
|
||||
:param context: request context, used only if image_href is Glance href.
|
||||
:raises: exception.ImageRefValidationFailed if no image service can
|
||||
handle specified href.
|
||||
:returns: Instance of an image service class that is able to download
|
||||
specified image.
|
||||
"""
|
||||
scheme = urlparse.urlparse(image_href).scheme.lower()
|
||||
try:
|
||||
cls = protocol_mapping[scheme or 'glance']
|
||||
except KeyError:
|
||||
raise exception.ImageRefValidationFailed(
|
||||
image_href=image_href,
|
||||
reason=_('Image download protocol '
|
||||
'%s is not supported.') % scheme
|
||||
)
|
||||
|
||||
if cls == GlanceImageService:
|
||||
return cls(client, version, context)
|
||||
return cls()
|
|
@ -1,5 +0,0 @@
|
|||
default boot
|
||||
|
||||
label boot
|
||||
kernel {{ kernel }}
|
||||
append initrd={{ ramdisk }} text {{ kernel_params }} --
|
|
@ -13,10 +13,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from iotronic.common import exception
|
||||
from iotronic.db import api as dbapi
|
||||
from iotronic.objects import base
|
||||
from iotronic.objects import utils as obj_utils
|
||||
|
@ -52,23 +48,9 @@ class Location(base.IotronicObject):
|
|||
cls(context),
|
||||
obj) for obj in db_objects]
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, context, location_id):
|
||||
"""Find a location based on its id or uuid and return a Location object.
|
||||
|
||||
:param location_id: the id *or* uuid of a location.
|
||||
:returns: a :class:`Location` object.
|
||||
"""
|
||||
if strutils.is_int_like(location_id):
|
||||
return cls.get_by_id(context, location_id)
|
||||
elif uuidutils.is_uuid_like(location_id):
|
||||
return cls.get_by_uuid(context, location_id)
|
||||
else:
|
||||
raise exception.InvalidIdentity(identity=location_id)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_id(cls, context, location_id):
|
||||
"""Find a location based on its integer id and return a Location object.
|
||||
"""Find a location based on its idand return a Location object.
|
||||
|
||||
:param location_id: the id of a location.
|
||||
:returns: a :class:`Location` object.
|
||||
|
@ -77,36 +59,53 @@ class Location(base.IotronicObject):
|
|||
location = Location._from_db_object(cls(context), db_location)
|
||||
return location
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_uuid(cls, context, uuid):
|
||||
"""Find a location based on uuid and return a :class:`Location` object.
|
||||
# @base.remotable_classmethod
|
||||
# def get(cls, context, location_id):
|
||||
# """Find a location based on its id or uuid and return
|
||||
# a Location object.
|
||||
#
|
||||
# :param location_id: the id *or* uuid of a location.
|
||||
# :returns: a :class:`Location` object.
|
||||
# """
|
||||
# if strutils.is_int_like(location_id):
|
||||
# return cls.get_by_id(context, location_id)
|
||||
# elif uuidutils.is_uuid_like(location_id):
|
||||
# return cls.get_by_uuid(context, location_id)
|
||||
# else:
|
||||
# raise exception.InvalidIdentity(identity=location_id)
|
||||
|
||||
:param uuid: the uuid of a location.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Location` object.
|
||||
"""
|
||||
db_location = cls.dbapi.get_location_by_uuid(uuid)
|
||||
location = Location._from_db_object(cls(context), db_location)
|
||||
return location
|
||||
# @base.remotable_classmethod
|
||||
# def get_by_uuid(cls, context, uuid):
|
||||
# """Find a location based on uuid and return a
|
||||
# :class:`Location` object.
|
||||
#
|
||||
# :param uuid: the uuid of a location.
|
||||
# :param context: Security context
|
||||
# :returns: a :class:`Location` object.
|
||||
# """
|
||||
# db_location = cls.dbapi.get_location_by_uuid(uuid)
|
||||
# location = Location._from_db_object(cls(context), db_location)
|
||||
# return location
|
||||
|
||||
@base.remotable_classmethod
|
||||
def list(cls, context, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
"""Return a list of Location objects.
|
||||
|
||||
:param context: Security context.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param sort_key: column to sort results by.
|
||||
:param sort_dir: direction to sort. "asc" or "desc".
|
||||
:returns: a list of :class:`Location` object.
|
||||
|
||||
"""
|
||||
db_locations = cls.dbapi.get_location_list(limit=limit,
|
||||
marker=marker,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
return Location._from_db_object_list(db_locations, cls, context)
|
||||
# @base.remotable_classmethod
|
||||
# def list(cls, context, limit=None, marker=None,
|
||||
# sort_key=None, sort_dir=None):
|
||||
# """Return a list of Location objects.
|
||||
#
|
||||
# :param context: Security context.
|
||||
# :param limit: maximum number of resources to return
|
||||
# in a single result.
|
||||
# :param marker: pagination marker for large data sets.
|
||||
# :param sort_key: column to sort results by.
|
||||
# :param sort_dir: direction to sort. "asc" or "desc".
|
||||
# :returns: a list of :class:`Location` object.
|
||||
#
|
||||
# """
|
||||
# db_locations = cls.dbapi.get_location_list(limit=limit,
|
||||
# marker=marker,
|
||||
# sort_key=sort_key,
|
||||
# sort_dir=sort_dir)
|
||||
# return Location._from_db_object_list(db_locations, cls, context)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def list_by_board_uuid(cls, context, board_uuid, limit=None, marker=None,
|
||||
|
@ -200,24 +199,24 @@ class Location(base.IotronicObject):
|
|||
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
def refresh(self, context=None):
|
||||
"""Loads updates for this Location.
|
||||
|
||||
Loads a location with the same uuid from the database and
|
||||
checks for updated attributes. Updates are applied from
|
||||
the loaded location column by column, if there are any updates.
|
||||
|
||||
:param context: Security context. NOTE: This should only
|
||||
be used internally by the indirection_api.
|
||||
Unfortunately, RPC requires context as the first
|
||||
argument, even though we don't use it.
|
||||
A context should be set when instantiating the
|
||||
object, e.g.: Location(context)
|
||||
"""
|
||||
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
|
||||
for field in self.fields:
|
||||
if (hasattr(
|
||||
self, base.get_attrname(field))
|
||||
and self[field] != current[field]):
|
||||
self[field] = current[field]
|
||||
# @base.remotable
|
||||
# def refresh(self, context=None):
|
||||
# """Loads updates for this Location.
|
||||
#
|
||||
# Loads a location with the same uuid from the database and
|
||||
# checks for updated attributes. Updates are applied from
|
||||
# the loaded location column by column, if there are any updates.
|
||||
#
|
||||
# :param context: Security context. NOTE: This should only
|
||||
# be used internally by the indirection_api.
|
||||
# Unfortunately, RPC requires context as the first
|
||||
# argument, even though we don't use it.
|
||||
# A context should be set when instantiating the
|
||||
# object, e.g.: Location(context)
|
||||
# """
|
||||
# current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
|
||||
# for field in self.fields:
|
||||
# if (hasattr(
|
||||
# self, base.get_attrname(field))
|
||||
# and self[field] != current[field]):
|
||||
# self[field] = current[field]
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2010-2011 OpenStack Foundation
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslotest import base
|
||||
|
||||
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
||||
"""Test case base class for all unit tests."""
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
test_iotronic
|
||||
----------------------------------
|
||||
|
||||
Tests for `iotronic` module.
|
||||
"""
|
||||
|
||||
from iotronic.tests import base
|
||||
|
||||
|
||||
class TestIotronic(base.TestCase):
|
||||
|
||||
def test_something(self):
|
||||
pass
|
|
@ -194,7 +194,7 @@ SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
|
|||
-- insert testing boards
|
||||
INSERT INTO `boards` VALUES
|
||||
('2017-02-20 10:38:26',NULL,132,'f3961f7a-c937-4359-8848-fb64aa8eeaaa','12345','registered','laptop-14','server',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}'),
|
||||
('2017-02-20 10:38:45',NULL,133,'ba1efce9-cad9-4ae1-a5d1-d90a8d203d3b','yunyun','registered','yun22','yun',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}'),
|
||||
('2017-02-20 10:38:45',NULL,133,'e9bee8d9-7270-5323-d3e9-9875ba9c5753','yunyun','registered','yun22','yun',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}'),
|
||||
('2017-02-20 10:39:08',NULL,134,'65f9db36-9786-4803-b66f-51dcdb60066e','test','registered','test','server',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}');
|
||||
INSERT INTO `locations` VALUES
|
||||
('2017-02-20 10:38:26',NULL,6,'2','1','3',132),
|
||||
|
@ -206,4 +206,4 @@ p1
|
|||
.',0,'{}','eee383360cc14c44b9bf21e1e003a4f3')
|
||||
('2017-02-20 10:38:26',NULL,133,'edff22cd-9148-4ad8-b35b-c0c80abf1e8a','zero','0','Vfrom iotronic_lightningrod.plugins import Plugin\u000a\u000afrom oslo_log import log as logging\u000a\u000aLOG = logging.getLogger(__name__)\u000a\u000a\u000a# User imports\u000a\u000a\u000aclass Worker(Plugin.Plugin):\u000a def __init__(self, name, is_running):\u000a super(Worker, self).__init__(name, is_running)\u000a\u000a def run(self):\u000a LOG.info("Plugin process completed!")\u000a #self.Done()
|
||||
p1
|
||||
.',1,'{}','eee383360cc14c44b9bf21e1e003a4f3');
|
||||
.',1,'{}','eee383360cc14c44b9bf21e1e003a4f3');
|
||||
|
|
Loading…
Reference in New Issue