Add support for packet json metadata service

Packet is a NYC-based infrastructure startup, focused on
reinventing how SaaS/PaaS companies go global with premium
bare metal and container hosting.

This commit adds the following features:

    - ssh keys addition
    - userdata execution
    - hostname setting

Partially implements: blueprint packet-metadata-suport
Co-Authored-By: Alexandru Coman <acoman@cloudbasesolutions.com>
Co-Authored-By: Stefan Caraiman <scaraiman@cloudbasesolutions.com>
Co-Authored-By: Paula Madalina Crismaru <pcrismaru@cloudbasesolutions.com>

Change-Id: I80b4f8b91fd8ef2b63735d5569a8009e34cee759
This commit is contained in:
Adrian Vladu 2017-07-05 13:56:41 +03:00
parent 8a03b4700e
commit 526d939240
4 changed files with 259 additions and 0 deletions

View File

@ -23,6 +23,7 @@ _OPT_PATHS = (
'cloudbaseinit.conf.openstack.OpenStackOptions',
'cloudbaseinit.conf.azure.AzureOptions',
'cloudbaseinit.conf.ovf.OvfOptions',
'cloudbaseinit.conf.packet.PacketOptions',
)

View File

@ -0,0 +1,50 @@
# Copyright 2016 Cloudbase Solutions Srl
#
# 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.
"""Config options available for the Packet metadata service."""
from oslo_config import cfg
from cloudbaseinit.conf import base as conf_base
class PacketOptions(conf_base.Options):
"""Config options available for the Packet metadata service."""
def __init__(self, config):
super(PacketOptions, self).__init__(config, group="packet")
self._options = [
cfg.StrOpt(
'metadata_base_url', default="https://metadata.packet.net/",
help='The URL where the service looks for metadata'),
cfg.BoolOpt(
"https_allow_insecure", default=False,
help="Whether to disable the validation of HTTPS "
"certificates."),
cfg.StrOpt(
"https_ca_bundle", default=None,
help="The path to a CA_BUNDLE file or directory with "
"certificates of trusted CAs."),
]
def register(self):
"""Register the current options to the global ConfigOpts object."""
group = cfg.OptGroup(self.group_name, title='Packet Options')
self._config.register_group(group)
self._config.register_opts(self._options, group=group)
def list(self):
"""Return a list which contains all the available options."""
return self._options

View File

@ -0,0 +1,85 @@
# Copyright 2016 Cloudbase Solutions Srl
#
# 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.
"""Metadata Service for Packet."""
import json
from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit.metadata.services import base
from oslo_log import log as oslo_logging
CONF = cloudbaseinit_conf.CONF
LOG = oslo_logging.getLogger(__name__)
class PacketService(base.BaseHTTPMetadataService):
"""Metadata Service for Packet.
Packet is a NYC-based infrastructure startup, focused on reinventing
how SaaS/PaaS companies go global with premium bare metal and container
hosting.
"""
def __init__(self):
super(PacketService, self).__init__(
base_url=CONF.packet.metadata_base_url,
https_allow_insecure=CONF.packet.https_allow_insecure,
https_ca_bundle=CONF.packet.https_ca_bundle)
self._enable_retry = True
def _get_meta_data(self):
data = self._get_cache_data("metadata", decode=True)
if data:
return json.loads(data)
def load(self):
"""Load all the available information from the metadata service."""
super(PacketService, self).load()
try:
self._get_meta_data()
return True
except Exception:
LOG.debug('Metadata not found at URL \'%s\'' %
CONF.packet.metadata_base_url)
return False
def get_instance_id(self):
"""Get the identifier for the current instance.
The instance identifier provides an unique way to address an
instance into the current metadata provider.
"""
return self._get_meta_data().get("id")
def get_host_name(self):
"""Get the hostname for the current instance.
The hostname is the label assigned to the current instance used to
identify it in various forms of electronic communication.
"""
return self._get_meta_data().get("hostname")
def get_public_keys(self):
"""Get a list of space-stripped strings as public keys."""
meta_data = self._get_meta_data()
ssh_keys = meta_data.get("ssh_keys")
if not ssh_keys:
return []
return list(set((key.strip() for key in ssh_keys)))
def get_user_data(self):
"""Get the available user data for the current instance."""
return self._get_cache_data("userdata")

View File

@ -0,0 +1,123 @@
# Copyright 2017 Cloudbase Solutions Srl
#
# 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 importlib
import unittest
try:
import unittest.mock as mock
except ImportError:
import mock
from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit.tests import testutils
CONF = cloudbaseinit_conf.CONF
BASE_MODULE_PATH = ("cloudbaseinit.metadata.services.base."
"BaseHTTPMetadataService")
MODULE_PATH = "cloudbaseinit.metadata.services.packet"
class PacketServiceTest(unittest.TestCase):
def setUp(self):
self._win32com_mock = mock.MagicMock()
self._ctypes_mock = mock.MagicMock()
self._ctypes_util_mock = mock.MagicMock()
self._win32com_client_mock = mock.MagicMock()
self._pywintypes_mock = mock.MagicMock()
self._module_patcher = mock.patch.dict(
'sys.modules',
{'win32com': self._win32com_mock,
'ctypes': self._ctypes_mock,
'ctypes.util': self._ctypes_util_mock,
'win32com.client': self._win32com_client_mock,
'pywintypes': self._pywintypes_mock})
self._module_patcher.start()
self.addCleanup(self._module_patcher.stop)
self._packet_module = importlib.import_module(MODULE_PATH)
self._packet_service = self._packet_module.PacketService()
self.snatcher = testutils.LogSnatcher(MODULE_PATH)
@mock.patch(MODULE_PATH + ".PacketService._get_cache_data")
def test_get_meta_data(self, mock_get_cache_data):
mock_get_cache_data.return_value = '{"fake": "data"}'
response = self._packet_service._get_meta_data()
mock_get_cache_data.assert_called_with("metadata", decode=True)
self.assertEqual({"fake": "data"}, response)
@mock.patch(BASE_MODULE_PATH + ".load")
@mock.patch(MODULE_PATH + ".PacketService._get_cache_data")
def test_load(self, mock_get_cache_data, mock_load):
mock_get_cache_data.return_value = '{"fake": "data"}'
self.assertTrue(self._packet_service.load())
@mock.patch(BASE_MODULE_PATH + ".load")
@mock.patch(MODULE_PATH + ".PacketService._get_cache_data")
def test_load_fails(self, mock_get_cache_data, mock_load):
with testutils.LogSnatcher(MODULE_PATH) as snatcher:
self.assertFalse(self._packet_service.load())
self.assertEqual(snatcher.output,
['Metadata not found at URL \'%s\'' %
CONF.packet.metadata_base_url])
@mock.patch(MODULE_PATH + ".PacketService._get_meta_data")
def test_get_instance_id(self, mock_get_meta_data):
response = self._packet_service.get_instance_id()
mock_get_meta_data.assert_called_once_with()
mock_get_meta_data().get.assert_called_once_with('id')
self.assertEqual(mock_get_meta_data.return_value.get.return_value,
response)
@mock.patch(MODULE_PATH +
".PacketService._get_meta_data")
def test_get_host_name(self, mock_get_meta_data):
response = self._packet_service.get_host_name()
mock_get_meta_data.assert_called_once_with()
mock_get_meta_data().get.assert_called_once_with('hostname')
self.assertEqual(mock_get_meta_data.return_value.get.return_value,
response)
@mock.patch(MODULE_PATH +
".PacketService._get_meta_data")
def _test_get_public_keys(self, mock_get_meta_data,
public_keys):
mock_get_meta_data.return_value = {
"ssh_keys": public_keys
}
response = self._packet_service.get_public_keys()
mock_get_meta_data.assert_called_once_with()
if public_keys:
public_keys = list(set((key.strip() for key in public_keys)))
else:
public_keys = []
self.assertEqual(sorted(public_keys),
sorted(response))
def test_get_public_keys(self):
self._test_get_public_keys(public_keys=["fake keys"] * 3)
def test_get_public_keys_empty(self):
self._test_get_public_keys(public_keys=None)
@mock.patch(MODULE_PATH +
".PacketService._get_cache_data")
def test_get_user_data(self, mock_get_cache_data):
response = self._packet_service.get_user_data()
mock_get_cache_data.assert_called_once_with("userdata")
self.assertEqual(mock_get_cache_data.return_value, response)