unused stuff have been removed

Change-Id: I413f23ad0882b918283334395efa525285d3d440
This commit is contained in:
Fabio Verboso 2017-03-23 16:12:47 +01:00
parent b9e45b1eee
commit e575619368
17 changed files with 69 additions and 1550 deletions

View File

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

View File

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

View File

@ -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.
"""

View File

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

View File

@ -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')

View File

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

View File

@ -1,8 +0,0 @@
set default=0
set timeout=5
set hidden_timeout_quiet=false
menuentry "boot_partition" {
linuxefi {{ linux }} {{ kernel_params }} --
initrdefi {{ initrd }}
}

View File

@ -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()

View File

@ -1,5 +0,0 @@
default boot
label boot
kernel {{ kernel }}
append initrd={{ ramdisk }} text {{ kernel_params }} --

View File

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

View File

@ -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."""

View File

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

View File

@ -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');