Browse Source

Store and expose introspection data

This adds the ability to store all of the data collected
during introspection. The configuration option
"[processing] store_data" (defaults to 'none'), determines
this behavior. Initially, only 'none' and 'swift' are
supported. If 'swift' is used, the data is stored in Swift
with the object name of "inspector_data-<UUID>".

Adds an endpoint /v1/introspection/<UUID>/data which
retrieves the data according to the method in
"[processing] store_data". Returns 404 if this option
is disabled.

There is a further option to store the location of the data
in the Ironic Node.extra column. For 'swift', this will be
the name of the swift object. The option,
"[processing] store_data_location" determines the key
name in the Node.extra column. (defaults to not storing
the location).

Change-Id: Ibc38064f7ea56f85b9f5a77ef6f62a50f0381ff4
Implements: blueprint store-introspection-data
changes/59/213159/13
John Trowbridge 7 years ago
parent
commit
6eb9f58c87
  1. 1
      CONTRIBUTING.rst
  2. 18
      HTTP-API.rst
  3. 17
      devstack/exercise.sh
  4. 11
      devstack/plugin.sh
  5. 9
      example.conf
  6. 54
      ironic_inspector/common/swift.py
  7. 12
      ironic_inspector/conf.py
  8. 17
      ironic_inspector/main.py
  9. 12
      ironic_inspector/process.py
  10. 31
      ironic_inspector/test/test_main.py
  11. 35
      ironic_inspector/test/test_process.py
  12. 78
      ironic_inspector/test/test_swift.py
  13. 2
      plugin-requirements.txt
  14. 1
      requirements.txt

1
CONTRIBUTING.rst

@ -83,6 +83,7 @@ Example local.conf
enable_service ironic ir-api ir-cond
disable_service n-net n-novnc
enable_service neutron q-svc q-agt q-dhcp q-l3 q-meta
enable_service s-proxy s-object s-container s-account
disable_service heat h-api h-api-cfn h-api-cw h-eng
disable_service cinder c-sch c-api c-vol

18
HTTP-API.rst

@ -50,6 +50,23 @@ Response body: JSON dictionary with keys:
* ``finished`` (boolean) whether introspection is finished
* ``error`` error string or ``null``
Get Introspection Data
~~~~~~~~~~~~~~~~~~~~~~
``GET /v1/introspection/<UUID>/data`` get stored data from successful
introspection.
Requires X-Auth-Token header with Keystone token for authentication.
Response:
* 200 - OK
* 400 - bad request
* 401, 403 - missing or invalid authentication
* 404 - data cannot be found or data storage not configured
Response body: JSON dictionary with introspection data
Ramdisk Callback
~~~~~~~~~~~~~~~~
@ -151,3 +168,4 @@ Version History
^^^^^^^^^^^^^^^
**1.0** version of API at the moment of introducing versioning.
**1.1** adds endpoint to retrieve stored introspection data.

17
devstack/exercise.sh

@ -71,6 +71,21 @@ function curl_ir {
curl -H "X-Auth-Token: $token" -X $1 "$ironic_url/$2"
}
function curl_ins {
curl -H "X-Auth-Token: $token" -X $1 "http://127.0.0.1:5050/$2"
}
function test_swift {
# Basic sanity check of the data stored in Swift
stored_data_json=$(curl_ins GET v1/introspection/$uuid/data)
stored_cpu_arch=$(echo $stored_data_json | jq -r '.cpu_arch')
echo CPU arch for $uuid from stored data: $stored_cpu_arch
if [ "$stored_cpu_arch" != "$expected_cpu_arch" ]; then
echo "The data stored in Swift does not match the expected data."
exit 1
fi
}
for uuid in $nodes; do
node_json=$(curl_ir GET v1/nodes/$uuid)
properties=$(echo $node_json | jq '.properties')
@ -93,6 +108,8 @@ for uuid in $nodes; do
exit 1
fi
openstack service list | grep swift && test_swift
for attempt in {1..12}; do
node_json=$(curl_ir GET v1/nodes/$uuid)
provision_state=$(echo $node_json | jq -r '.provision_state')

11
devstack/plugin.sh

@ -137,10 +137,21 @@ function configure_inspector {
inspector_iniset firewall dnsmasq_interface $IRONIC_INSPECTOR_INTERFACE
inspector_iniset database connection sqlite:///$IRONIC_INSPECTOR_DATA_DIR/inspector.sqlite
is_service_enabled swift && configure_inspector_swift
iniset "$IRONIC_CONF_FILE" inspector enabled True
iniset "$IRONIC_CONF_FILE" inspector service_url $IRONIC_INSPECTOR_URI
}
function configure_inspector_swift {
inspector_iniset swift os_auth_url "$KEYSTONE_SERVICE_URI/v2.0"
inspector_iniset swift username $IRONIC_INSPECTOR_ADMIN_USER
inspector_iniset swift password $SERVICE_PASSWORD
inspector_iniset swift tenant_name $SERVICE_TENANT_NAME
inspector_iniset processing store_data swift
}
function configure_inspector_dhcp {
mkdir_chown_stack "$IRONIC_INSPECTOR_CONF_DIR"

9
example.conf

@ -589,6 +589,15 @@
# ignored by default. (string value)
#node_not_found_hook = <None>
# Method for storing introspection data. If set to 'none',
# introspection data will not be stored (string value)
# Allowed values: none, swift
#store_data = none
# Name of the key to store the location of stored data in the extra
# column of the Ironic database. (string value)
#store_data_location = <None>
[swift]

54
ironic_inspector/common/swift.py

@ -13,6 +13,8 @@
# Mostly copied from ironic/common/swift.py
import json
from oslo_config import cfg
from oslo_log import log
from swiftclient import client as swift_client
@ -71,6 +73,8 @@ def list_opts():
CONF.register_opts(SWIFT_OPTS, group='swift')
OBJECT_NAME_PREFIX = 'inspector_data'
class SwiftAPI(object):
"""API for communicating with Swift."""
@ -137,3 +141,53 @@ class SwiftAPI(object):
raise utils.Error(err_msg)
return obj_uuid
def get_object(self, object, container=CONF.swift.container):
"""Downloads a given object from Swift.
:param object: The name of the object in Swift
:param container: The name of the container for the object.
:returns: Swift object
:raises: utils.Error, if the Swift operation fails.
"""
try:
headers, obj = self.connection.get_object(container, object)
except swift_exceptions.ClientException as e:
err_msg = (_('Swift failed to get object %(object)s in '
'container %(container)s. Error was: %(error)s') %
{'object': object, 'container': container, 'error': e})
raise utils.Error(err_msg)
return obj
def store_introspection_data(data, uuid):
"""Uploads introspection data to Swift.
:param data: data to store in Swift
:param uuid: UUID of the Ironic node that the data came from
:returns: name of the Swift object that the data is stored in
"""
swift_api = SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
swift_object_name = '%s-%s' % (OBJECT_NAME_PREFIX, uuid)
swift_api.create_object(swift_object_name, json.dumps(data))
return swift_object_name
def get_introspection_data(uuid):
"""Downloads introspection data from Swift.
:param uuid: UUID of the Ironic node that the data came from
:returns: Swift object with the introspection data
"""
swift_api = SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
swift_object_name = '%s-%s' % (OBJECT_NAME_PREFIX, uuid)
return swift_api.get_object(swift_object_name)

12
ironic_inspector/conf.py

@ -16,6 +16,7 @@ from oslo_config import cfg
VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe')
VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added')
VALID_STORE_DATA_VALUES = ('none', 'swift')
IRONIC_OPTS = [
@ -160,7 +161,16 @@ PROCESSING_OPTS = [
default=None,
help='The name of the hook to run when inspector receives '
'inspection information from a node it isn\'t already '
'aware of. This hook is ignored by default.')
'aware of. This hook is ignored by default.'),
cfg.StrOpt('store_data',
default='none',
choices=VALID_STORE_DATA_VALUES,
help='Method for storing introspection data. If set to \'none'
'\', introspection data will not be stored.'),
cfg.StrOpt('store_data_location',
default=None,
help='Name of the key to store the location of stored data in '
'the extra column of the Ironic database.'),
]

17
ironic_inspector/main.py

@ -26,6 +26,7 @@ from oslo_utils import uuidutils
from ironic_inspector import db
from ironic_inspector.common.i18n import _, _LC, _LE, _LI, _LW
from ironic_inspector.common import swift
from ironic_inspector import conf # noqa
from ironic_inspector import firewall
from ironic_inspector import introspect
@ -41,7 +42,7 @@ app = flask.Flask(__name__)
LOG = log.getLogger('ironic_inspector.main')
MINIMUM_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 1)
_MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version'
_MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version'
_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version'
@ -152,6 +153,20 @@ def api_introspection(uuid):
error=node_info.error or None)
@app.route('/v1/introspection/<uuid>/data', methods=['GET'])
@convert_exceptions
def api_introspection_data(uuid):
utils.check_auth(flask.request)
if CONF.processing.store_data == 'swift':
res = swift.get_introspection_data(uuid)
return res, 200, {'Content-Type': 'applications/json'}
else:
return error_response(_('Inspector is not configured to store data. '
'Set the [processing] store_data '
'configuration option to change this.'),
code=404)
@app.errorhandler(404)
def handle_404(error):
return error_response(error, code=404)

12
ironic_inspector/process.py

@ -15,14 +15,17 @@
import eventlet
from ironicclient import exceptions
from oslo_config import cfg
from oslo_log import log
from ironic_inspector.common.i18n import _, _LE, _LI
from ironic_inspector.common import swift
from ironic_inspector import firewall
from ironic_inspector import node_cache
from ironic_inspector.plugins import base as plugins_base
from ironic_inspector import utils
CONF = cfg.CONF
LOG = log.getLogger("ironic_inspector.process")
@ -142,6 +145,15 @@ def _process_node(ironic, node, introspection_data, node_info):
node_patches, port_patches = _run_post_hooks(node_info,
introspection_data)
if CONF.processing.store_data == 'swift':
swift_object_name = swift.store_introspection_data(introspection_data,
node_info.uuid)
if CONF.processing.store_data_location:
node_patches.append({'op': 'add',
'path': '/extra/%s' %
CONF.processing.store_data_location,
'value': swift_object_name})
node = ironic.node.update(node.uuid, node_patches)
for mac, patches in port_patches.items():
port = node_info.ports(ironic)[mac]

31
ironic_inspector/test/test_main.py

@ -150,6 +150,37 @@ class TestApiGetStatus(BaseAPITest):
json.loads(res.data.decode('utf-8')))
class TestApiGetData(BaseAPITest):
@mock.patch.object(main.swift, 'SwiftAPI', autospec=True)
def test_get_introspection_data(self, swift_mock):
CONF.set_override('store_data', 'swift', 'processing')
data = {
'ipmi_address': '1.2.3.4',
'cpus': 2,
'cpu_arch': 'x86_64',
'memory_mb': 1024,
'local_gb': 20,
'interfaces': {
'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
}
}
swift_conn = swift_mock.return_value
swift_conn.get_object.return_value = json.dumps(data)
res = self.app.get('/v1/introspection/%s/data' % self.uuid)
name = 'inspector_data-%s' % self.uuid
swift_conn.get_object.assert_called_once_with(name)
self.assertEqual(200, res.status_code)
self.assertEqual(data, json.loads(res.data.decode('utf-8')))
@mock.patch.object(main.swift, 'SwiftAPI', autospec=True)
def test_introspection_data_not_stored(self, swift_mock):
CONF.set_override('store_data', 'none', 'processing')
swift_conn = swift_mock.return_value
res = self.app.get('/v1/introspection/%s/data' % self.uuid)
self.assertFalse(swift_conn.get_object.called)
self.assertEqual(404, res.status_code)
class TestApiMisc(BaseAPITest):
@mock.patch.object(node_cache, 'get_node', autospec=True)
def test_404_expected(self, get_mock):

35
ironic_inspector/test/test_process.py

@ -12,6 +12,7 @@
# limitations under the License.
import functools
import json
import time
import eventlet
@ -578,6 +579,40 @@ class TestProcessNode(BaseTest):
self.cli.port.delete.assert_any_call(port.uuid)
self.assertEqual(2, self.cli.port.delete.call_count)
@mock.patch.object(process.swift, 'SwiftAPI', autospec=True)
def test_store_data(self, swift_mock, filters_mock, post_hook_mock):
CONF.set_override('store_data', 'swift', 'processing')
swift_conn = swift_mock.return_value
name = 'inspector_data-%s' % self.uuid
expected = json.dumps(self.data)
self.call()
swift_conn.create_object.assert_called_once_with(name, expected)
self.cli.node.update.assert_called_once_with(self.uuid,
self.patch_props)
@mock.patch.object(process.swift, 'SwiftAPI', autospec=True)
def test_store_data_location(self, swift_mock, filters_mock,
post_hook_mock):
CONF.set_override('store_data', 'swift', 'processing')
CONF.set_override('store_data_location', 'inspector_data_object',
'processing')
swift_conn = swift_mock.return_value
name = 'inspector_data-%s' % self.uuid
self.patch_props.append(
{'path': '/extra/inspector_data_object',
'value': name,
'op': 'add'}
)
expected = json.dumps(self.data)
self.call()
swift_conn.create_object.assert_called_once_with(name, expected)
self.cli.node.update.assert_called_once_with(self.uuid,
self.patch_props)
class TestValidateInterfacesHook(test_base.BaseTest):
def test_wrong_add_ports(self):

78
ironic_inspector/test/test_swift.py

@ -26,14 +26,34 @@ from swiftclient import client as swift_client
from swiftclient import exceptions as swift_exception
from ironic_inspector.common import swift
from ironic_inspector.test import base
from ironic_inspector.test import base as test_base
from ironic_inspector import utils
CONF = cfg.CONF
class BaseTest(test_base.NodeTest):
def setUp(self):
super(BaseTest, self).setUp()
self.all_macs = self.macs + ['DE:AD:BE:EF:DE:AD']
self.pxe_mac = self.macs[1]
self.data = {
'ipmi_address': self.bmc_address,
'cpus': 2,
'cpu_arch': 'x86_64',
'memory_mb': 1024,
'local_gb': 20,
'interfaces': {
'em1': {'mac': self.macs[0], 'ip': '1.2.0.1'},
'em2': {'mac': self.macs[1], 'ip': '1.2.0.2'},
'em3': {'mac': self.all_macs[2]},
},
'boot_interface': '01-' + self.pxe_mac.replace(':', '-'),
}
@mock.patch.object(swift_client, 'Connection', autospec=True)
class SwiftTestCase(base.BaseTest):
class SwiftTestCase(BaseTest):
def setUp(self):
super(SwiftTestCase, self).setUp()
@ -54,7 +74,11 @@ class SwiftTestCase(base.BaseTest):
reload_module(sys.modules['ironic_inspector.common.swift'])
def test___init__(self, connection_mock):
swift.SwiftAPI()
swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
params = {'retries': 2,
'user': 'swift',
'tenant_name': 'tenant',
@ -66,7 +90,11 @@ class SwiftTestCase(base.BaseTest):
connection_mock.assert_called_once_with(**params)
def test_create_object(self, connection_mock):
swiftapi = swift.SwiftAPI()
swiftapi = swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
connection_obj_mock = connection_mock.return_value
connection_obj_mock.put_object.return_value = 'object-uuid'
@ -80,7 +108,11 @@ class SwiftTestCase(base.BaseTest):
self.assertEqual('object-uuid', object_uuid)
def test_create_object_create_container_fails(self, connection_mock):
swiftapi = swift.SwiftAPI()
swiftapi = swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
connection_obj_mock = connection_mock.return_value
connection_obj_mock.put_container.side_effect = self.swift_exception
self.assertRaises(utils.Error, swiftapi.create_object, 'object',
@ -90,7 +122,11 @@ class SwiftTestCase(base.BaseTest):
self.assertFalse(connection_obj_mock.put_object.called)
def test_create_object_put_object_fails(self, connection_mock):
swiftapi = swift.SwiftAPI()
swiftapi = swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
connection_obj_mock = connection_mock.return_value
connection_obj_mock.put_object.side_effect = self.swift_exception
self.assertRaises(utils.Error, swiftapi.create_object, 'object',
@ -99,3 +135,33 @@ class SwiftTestCase(base.BaseTest):
'inspector')
connection_obj_mock.put_object.assert_called_once_with(
'ironic-inspector', 'object', 'some-string-data', headers=None)
def test_get_object(self, connection_mock):
swiftapi = swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
connection_obj_mock = connection_mock.return_value
expected_obj = self.data
connection_obj_mock.get_object.return_value = ('headers', expected_obj)
swift_obj = swiftapi.get_object('object')
connection_obj_mock.get_object.assert_called_once_with(
'ironic-inspector', 'object')
self.assertEqual(expected_obj, swift_obj)
def test_get_object_fails(self, connection_mock):
swiftapi = swift.SwiftAPI(user=CONF.swift.username,
tenant_name=CONF.swift.tenant_name,
key=CONF.swift.password,
auth_url=CONF.swift.os_auth_url,
auth_version=CONF.swift.os_auth_version)
connection_obj_mock = connection_mock.return_value
connection_obj_mock.get_object.side_effect = self.swift_exception
self.assertRaises(utils.Error, swiftapi.get_object,
'object')
connection_obj_mock.get_object.assert_called_once_with(
'ironic-inspector', 'object')

2
plugin-requirements.txt

@ -1,2 +0,0 @@
# required for extra_hardware plugin
python-swiftclient>=2.2.0

1
requirements.txt

@ -8,6 +8,7 @@ keystonemiddleware>=2.0.0
pbr<2.0,>=1.6
python-ironicclient>=0.6.0
python-keystoneclient>=1.6.0
python-swiftclient>=2.2.0
oslo.config>=2.3.0 # Apache-2.0
oslo.db>=2.4.1 # Apache-2.0
oslo.i18n>=1.5.0 # Apache-2.0

Loading…
Cancel
Save