Pass in InstanceConfig instead of ssh_keys

Change-Id: I7b313174896917d583bf8356207abf57ea08a197
This commit is contained in:
Dmitry Tantsur 2018-06-22 11:18:09 +02:00
parent 72995b1117
commit 703bddb775
7 changed files with 121 additions and 52 deletions

View File

@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from metalsmith._config import InstanceConfig
from metalsmith._instance import Instance
from metalsmith._provisioner import Provisioner
__all__ = ['Instance', 'Provisioner']
__all__ = ['Instance', 'InstanceConfig', 'Provisioner']

View File

@ -19,6 +19,7 @@ import sys
from openstack import config as os_config
from metalsmith import _config
from metalsmith import _format
from metalsmith import _provisioner
from metalsmith import _utils
@ -51,12 +52,14 @@ def _do_deploy(api, args, formatter):
if args.hostname and not _utils.is_hostname_safe(args.hostname):
raise RuntimeError("%s cannot be used as a hostname" % args.hostname)
config = _config.InstanceConfig(ssh_keys=ssh_keys)
node = api.reserve_node(args.resource_class, capabilities=capabilities)
instance = api.provision_node(node,
image=args.image,
nics=args.nics,
root_disk_size=args.root_disk_size,
ssh_keys=ssh_keys,
config=config,
hostname=args.hostname,
netboot=args.netboot,
wait=wait)

63
metalsmith/_config.py Normal file
View File

@ -0,0 +1,63 @@
# Copyright 2018 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import json
import os
import shutil
import tempfile
class InstanceConfig(object):
"""Configuration of the target instance.
The information attached to this object will be passed via a configdrive
to the instance's first boot script (e.g. cloud-init).
:ivar ssh_keys: List of SSH public keys.
"""
def __init__(self, ssh_keys=None):
self.ssh_keys = ssh_keys or []
@contextlib.contextmanager
def build_configdrive_directory(self, node, hostname):
"""Build a configdrive from the provided information.
:param node: `Node` object.
:param hostname: instance hostname.
:return: a context manager yielding a directory with files
"""
d = tempfile.mkdtemp()
try:
metadata = {'public_keys': self.ssh_keys,
'uuid': node.uuid,
'name': node.name,
'hostname': hostname,
'launch_index': 0,
'availability_zone': '',
'files': [],
'meta': {}}
for version in ('2012-08-10', 'latest'):
subdir = os.path.join(d, 'openstack', version)
if not os.path.exists(subdir):
os.makedirs(subdir)
with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp:
json.dump(metadata, fp)
yield d
finally:
shutil.rmtree(d)

View File

@ -19,6 +19,7 @@ import sys
import six
from metalsmith import _config
from metalsmith import _instance
from metalsmith import _os_api
from metalsmith import _scheduler
@ -144,7 +145,7 @@ class Provisioner(object):
return hostname
def provision_node(self, node, image, nics=None, root_disk_size=None,
ssh_keys=None, hostname=None, netboot=False, wait=None):
config=None, hostname=None, netboot=False, wait=None):
"""Provision the node with the given image.
Example::
@ -165,8 +166,8 @@ class Provisioner(object):
to create a port on (``{"network": "<network name or ID>"}``).
:param root_disk_size: The size of the root partition. By default
the value of the local_gb property is used.
:param ssh_keys: list of public parts of the SSH keys to upload
to the nodes.
:param config: :py:class:`metalsmith.InstanceConfig` object with
the configuration to pass to the instance.
:param hostname: Hostname to assign to the instance. Defaults to the
node's name or UUID.
:param netboot: Whether to use networking boot for final instances.
@ -177,6 +178,8 @@ class Provisioner(object):
is already finished.
:raises: :py:class:`metalsmith.exceptions.Error`
"""
if config is None:
config = _config.InstanceConfig()
node = self._check_node_for_deploy(node)
created_ports = []
attached_ports = []
@ -225,7 +228,7 @@ class Provisioner(object):
LOG.debug('Generating a configdrive for node %s',
_utils.log_node(node))
with _utils.config_drive_dir(node, ssh_keys, hostname) as cd:
with config.build_configdrive_directory(node, hostname) as cd:
self._api.node_action(node, 'active',
configdrive=cd)
except Exception:

View File

@ -14,12 +14,7 @@
# limitations under the License.
import collections
import contextlib
import json
import os
import re
import shutil
import tempfile
import six
@ -47,31 +42,6 @@ def get_capabilities(node):
return caps
@contextlib.contextmanager
def config_drive_dir(node, ssh_keys, hostname):
d = tempfile.mkdtemp()
try:
metadata = {'public_keys': ssh_keys,
'uuid': node.uuid,
'name': node.name,
'hostname': hostname,
'launch_index': 0,
'availability_zone': '',
'files': [],
'meta': {}}
for version in ('2012-08-10', 'latest'):
subdir = os.path.join(d, 'openstack', version)
if not os.path.exists(subdir):
os.makedirs(subdir)
with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp:
json.dump(metadata, fp)
yield d
finally:
shutil.rmtree(d)
def get_root_disk(root_disk_size, node):
"""Validate and calculate the root disk size."""
if root_disk_size is not None:

View File

@ -61,10 +61,12 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
config = mock_pr.return_value.provision_node.call_args[1]['config']
self.assertEqual([], config.ssh_keys)
mock_log.basicConfig.assert_called_once_with(level=mock_log.WARNING,
format=mock.ANY)
self.assertEqual(
@ -104,7 +106,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -176,7 +178,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -198,7 +200,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -228,7 +230,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -260,7 +262,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -290,7 +292,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -320,7 +322,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -375,7 +377,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -400,10 +402,12 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=['foo'],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
config = mock_pr.return_value.provision_node.call_args[1]['config']
self.assertEqual(['foo'], config.ssh_keys)
def test_args_port(self, mock_os_conf, mock_pr):
args = ['deploy', '--port', 'myport', '--image', 'myimg',
@ -421,7 +425,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'port': 'myport'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -441,7 +445,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=None,
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -464,7 +468,7 @@ class TestDeploy(testtools.TestCase):
nics=[{'network': 'net1'}, {'port': 'port1'},
{'port': 'port2'}, {'network': 'net2'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=1800)
@ -485,7 +489,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=None,
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname='host',
netboot=False,
wait=1800)
@ -506,7 +510,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=3600)
@ -527,7 +531,7 @@ class TestDeploy(testtools.TestCase):
image='myimg',
nics=[{'network': 'mynet'}],
root_disk_size=None,
ssh_keys=[],
config=mock.ANY,
hostname=None,
netboot=False,
wait=None)

View File

@ -16,6 +16,7 @@
import mock
import testtools
from metalsmith import _config
from metalsmith import _instance
from metalsmith import _os_api
from metalsmith import _provisioner
@ -134,6 +135,30 @@ class TestProvisionNode(Base):
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_with_config(self):
config = mock.MagicMock(spec=_config.InstanceConfig)
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}],
config=config)
self.assertEqual(inst.uuid, self.node.uuid)
self.assertEqual(inst.node, self.node)
config.build_configdrive_directory.assert_called_once_with(
self.node, self.node.name)
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
self.api.attach_port_to_node.assert_called_once_with(
self.node.uuid, self.api.create_port.return_value.id)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_with_hostname(self):
hostname = 'control-0.example.com'
inst = self.pr.provision_node(self.node, 'image',