Don't create cloud-init user unless specified

When the the instance_user value from heat.conf is set to empty string/None and
the user doesn't specify Server's admin_user property, Heat will not create a
custom cloud-init user.

The instance_user config option and admin_user property are deprecated and will
be removed in Juno where this behaviour becomes the default.

AWS::EC2::Instance will still create a cloud-init user for CloudFormation
compatibility. In the absence of the instance_user config option, 'ec2-user'
will be used.

Closes-Bug: #1257410
Change-Id: I42dda36045f79be079b2030669284e9db71463d7
changes/78/79678/5
Tomas Sedovic 9 years ago
parent 0d19394458
commit b8eefd1de9
  1. 6
      etc/heat/heat.conf.sample
  2. 13
      heat/cloudinit/boothook.sh
  3. 6
      heat/cloudinit/config
  4. 12
      heat/common/config.py
  5. 15
      heat/engine/resources/instance.py
  6. 38
      heat/engine/resources/nova_utils.py
  7. 17
      heat/engine/resources/server.py
  8. 6
      heat/tests/test_engine_service.py
  9. 59
      heat/tests/test_instance.py
  10. 12
      heat/tests/test_instance_network.py
  11. 6
      heat/tests/test_nokey.py
  12. 48
      heat/tests/test_nova_utils.py
  13. 93
      heat/tests/test_server.py
  14. 6
      heat/tests/test_server_tags.py

@ -14,7 +14,11 @@
# Options defined in heat.common.config
#
# The default user for new instances. (string value)
# The default user for new instances. This option is
# deprecated and will be removed in the Juno release. If it's
# empty, Heat will use the default user set up with your cloud
# image (for OS::Nova::Server) or 'ec2-user' (for
# AWS::EC2::Instance). (string value)
#instance_user=ec2-user
# Driver to use for controlling instances. (string value)

@ -1,11 +1,14 @@
#!/bin/bash
# FIXME(shadower) The `useradd` and `sudoers` lines are a workaround for
# cloud-init 0.6.3 present in Ubuntu 12.04 LTS:
# FIXME(shadower) this is a workaround for cloud-init 0.6.3 present in Ubuntu
# 12.04 LTS:
# https://bugs.launchpad.net/heat/+bug/1257410
# Once we drop support for it, we can safely remove them.
useradd -m @INSTANCE_USER@
echo -e '@INSTANCE_USER@\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
#
# The old cloud-init doesn't create the users directly so the commands to do
# this are injected though nova_utils.py.
#
# Once we drop support for 0.6.3, we can safely remove this.
${add_custom_user}
# in case heat-cfntools has been installed from package but no symlinks
# are yet in /opt/aws/bin/

@ -1,8 +1,4 @@
# Set the SSH key provided by Nova to this user.
# On cloud-init 0.7.x (anything except Ubuntu 12.04 LTS which ships 0.6.3) this
# also creates the user and sets up passwordless sudo if the user isn't present
# already.
user: @INSTANCE_USER@
${add_custom_user}
# Capture all subprocess output into a logfile
# Useful for troubleshooting cloud-init issues

@ -28,6 +28,8 @@ from heat.common import wsgi
from heat.openstack.common import log as logging
from heat.openstack.common import rpc
logger = logging.getLogger(__name__)
paste_deploy_group = cfg.OptGroup('paste_deploy')
paste_deploy_opts = [
cfg.StrOpt('flavor',
@ -80,7 +82,11 @@ service_opts = [
engine_opts = [
cfg.StrOpt('instance_user',
default='ec2-user',
help='The default user for new instances.'),
help="The default user for new instances. This option "
"is deprecated and will be removed in the Juno release. "
"If it's empty, Heat will use the default user set up "
"with your cloud image (for OS::Nova::Server) or "
"'ec2-user' (for AWS::EC2::Instance)."),
cfg.StrOpt('instance_driver',
default='heat.engine.nova',
help='Driver to use for controlling instances.'),
@ -213,6 +219,10 @@ allowed_rpc_exception_modules.append('heat.common.exception')
cfg.CONF.set_default(name='allowed_rpc_exception_modules',
default=allowed_rpc_exception_modules)
if cfg.CONF.instance_user:
logger.warn(_('The "instance_user" option in heat.conf is deprecated and '
'will be removed in the Juno release.'))
def _get_deployment_flavor():
"""

@ -12,6 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo.config import cfg
cfg.CONF.import_opt('instance_user', 'heat.common.config')
from heat.common import exception
from heat.engine import clients
from heat.engine import constraints
@ -430,6 +434,14 @@ class Instance(resource.Resource):
subnet_id=self.properties[self.SUBNET_ID])
server = None
# FIXME(shadower): the instance_user config option is deprecated. Once
# it's gone, we should always use ec2-user for compatibility with
# CloudFormation.
if cfg.CONF.instance_user:
instance_user = cfg.CONF.instance_user
else:
instance_user = 'ec2-user'
try:
server = self.nova().servers.create(
name=self.physical_resource_name(),
@ -437,7 +449,8 @@ class Instance(resource.Resource):
flavor=flavor_id,
key_name=self.properties[self.KEY_NAME],
security_groups=security_groups,
userdata=nova_utils.build_userdata(self, userdata),
userdata=nova_utils.build_userdata(self, userdata,
instance_user),
meta=self._get_nova_metadata(self.properties),
scheduler_hints=scheduler_hints,
nics=nics,

@ -19,6 +19,7 @@ from email.mime.text import MIMEText
import json
import os
import pkgutil
import string
from oslo.config import cfg
import six
@ -187,16 +188,33 @@ def build_userdata(resource, userdata=None, instance_user=None,
return msg
def read_cloudinit_file(fn):
data = pkgutil.get_data('heat', 'cloudinit/%s' % fn)
data = data.replace('@INSTANCE_USER@',
instance_user or cfg.CONF.instance_user)
return data
attachments = [(read_cloudinit_file('config'), 'cloud-config'),
(read_cloudinit_file('boothook.sh'), 'boothook.sh',
'cloud-boothook')]
attachments.append((read_cloudinit_file('part_handler.py'),
'part-handler.py'))
return pkgutil.get_data('heat', 'cloudinit/%s' % fn)
if instance_user:
config_custom_user = 'user: %s' % instance_user
# FIXME(shadower): compatibility workaround for cloud-init 0.6.3. We
# can drop this once we stop supporting 0.6.3 (which ships with Ubuntu
# 12.04 LTS).
#
# See bug https://bugs.launchpad.net/heat/+bug/1257410
boothook_custom_user = r"""useradd -m %s
echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
""" % (instance_user, instance_user)
else:
config_custom_user = ''
boothook_custom_user = ''
cloudinit_config = string.Template(
read_cloudinit_file('config')).safe_substitute(
add_custom_user=config_custom_user)
cloudinit_boothook = string.Template(
read_cloudinit_file('boothook.sh')).safe_substitute(
add_custom_user=boothook_custom_user)
attachments = [(cloudinit_config, 'cloud-config'),
(cloudinit_boothook, 'boothook.sh', 'cloud-boothook'),
(read_cloudinit_file('part_handler.py'),
'part-handler.py')]
if is_cfntools:
attachments.append((userdata, 'cfn-userdata', 'x-cfninitdata'))

@ -171,8 +171,12 @@ class Server(stack_user.StackUser):
),
ADMIN_USER: properties.Schema(
properties.Schema.STRING,
_('Name of the administrative user to use on the server.'),
default=cfg.CONF.instance_user
_('Name of the administrative user to use on the server. '
'This property will be removed from Juno in favor of the '
'default cloud-init user set up for each image (e.g. "ubuntu" '
'for Ubuntu 12.04+, "fedora" for Fedora 19+ and "cloud-user" '
'for CentOS/RHEL 6.5).'),
support_status=support.SupportStatus(status=support.DEPRECATED)
),
AVAILABILITY_ZONE: properties.Schema(
properties.Schema.STRING,
@ -470,10 +474,17 @@ class Server(stack_user.StackUser):
if self.user_data_software_config():
self._create_transport_credentials()
if self.properties[self.ADMIN_USER]:
instance_user = self.properties[self.ADMIN_USER]
elif cfg.CONF.instance_user:
instance_user = cfg.CONF.instance_user
else:
instance_user = None
userdata = nova_utils.build_userdata(
self,
ud_content,
instance_user=self.properties[self.ADMIN_USER],
instance_user=instance_user,
user_data_format=user_data_format)
flavor = self.properties[self.FLAVOR]

@ -195,11 +195,13 @@ def setup_mocks(mocks, stack):
instance = stack['WebServer']
user_data = instance.properties['UserData']
server_userdata = nova_utils.build_userdata(instance, user_data)
server_userdata = nova_utils.build_userdata(instance, user_data,
'ec2-user')
mocks.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(server_userdata)
instance.t['Properties']['UserData'],
'ec2-user').AndReturn(server_userdata)
mocks.StubOutWithMock(fc.servers, 'create')
fc.servers.create(image=744, flavor=3, key_name='test',

@ -93,16 +93,6 @@ class InstancesTest(HeatTestCase):
instance.t = instance.stack.resolve_runtime_data(instance.t)
if stub_create:
# need to resolve the template functions
server_userdata = nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData'])
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(
server_userdata)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1, flavor=1, key_name='test',
@ -111,7 +101,7 @@ class InstancesTest(HeatTestCase):
instance.name,
limit=instance.physical_resource_name_limit),
security_groups=None,
userdata=server_userdata, scheduler_hints=None,
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=None, nics=None, availability_zone=None).AndReturn(
return_server)
@ -855,3 +845,50 @@ class InstancesTest(HeatTestCase):
'wo_ipaddr')
self.assertEqual('0.0.0.0', instance.FnGetAtt('PrivateIp'))
def test_default_instance_user(self):
"""The default value for instance_user in heat.conf is ec2-user."""
return_server = self.fc.servers.list()[1]
instance = self._setup_test_instance(return_server, 'default_user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(instance, 'wordpress', 'ec2-user')
self.m.ReplayAll()
scheduler.TaskRunner(instance.create)()
self.m.VerifyAll()
def test_custom_instance_user(self):
"""Test instance_user in heat.conf being set to a custom value.
Launching the instance should call build_userdata with the custom user
name.
This option is deprecated and will be removed in Juno.
"""
return_server = self.fc.servers.list()[1]
instance = self._setup_test_instance(return_server, 'custom_user')
self.m.StubOutWithMock(instances.cfg.CONF, 'instance_user')
instances.cfg.CONF.instance_user = 'custom_user'
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(instance, 'wordpress', 'custom_user')
self.m.ReplayAll()
scheduler.TaskRunner(instance.create)()
self.m.VerifyAll()
def test_empty_instance_user(self):
"""Test instance_user in heat.conf being empty.
Launching the instance should call build_userdata with
"ec2-user".
This behaviour is compatible with CloudFormation and will be
the default in Juno once the instance_user option gets removed.
"""
return_server = self.fc.servers.list()[1]
instance = self._setup_test_instance(return_server, 'empty_user')
self.m.StubOutWithMock(instances.cfg.CONF, 'instance_user')
instances.cfg.CONF.instance_user = ''
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(instance, 'wordpress', 'ec2-user')
self.m.ReplayAll()
scheduler.TaskRunner(instance.create)()
self.m.VerifyAll()

@ -178,11 +178,13 @@ class instancesTest(HeatTestCase):
# need to resolve the template functions
server_userdata = nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData'])
instance.t['Properties']['UserData'],
'ec2-user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(server_userdata)
instance.t['Properties']['UserData'],
'ec2-user').AndReturn(server_userdata)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
@ -232,11 +234,13 @@ class instancesTest(HeatTestCase):
# need to resolve the template functions
server_userdata = nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData'])
instance.t['Properties']['UserData'],
'ec2-user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(server_userdata)
instance.t['Properties']['UserData'],
'ec2-user').AndReturn(server_userdata)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(

@ -68,11 +68,13 @@ class nokeyTest(HeatTestCase):
# need to resolve the template functions
server_userdata = nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData'])
instance.t['Properties']['UserData'],
'ec2-user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(server_userdata)
instance.t['Properties']['UserData'],
'ec2-user').AndReturn(server_userdata)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(

@ -161,17 +161,6 @@ class NovaUtilsRefreshServerTests(HeatTestCase):
class NovaUtilsUserdataTests(HeatTestCase):
scenarios = [
('no_conf_no_prop', dict(
conf_user='ec2-user', instance_user=None, expect='ec2-user')),
('no_conf_prop', dict(
conf_user='ec2-user', instance_user='fruity', expect='fruity')),
('conf_no_prop', dict(
conf_user='nutty', instance_user=None, expect='nutty')),
('conf_prop', dict(
conf_user='nutty', instance_user='fruity', expect='fruity')),
]
def setUp(self):
super(NovaUtilsUserdataTests, self).setUp()
self.nova_client = self.m.CreateMockAnything()
@ -182,14 +171,12 @@ class NovaUtilsUserdataTests(HeatTestCase):
resource.metadata = {}
self.m.StubOutWithMock(nova_utils.cfg, 'CONF')
cnf = nova_utils.cfg.CONF
cnf.instance_user = self.conf_user
cnf.heat_metadata_server_url = 'http://server.test:123'
cnf.heat_watch_server_url = 'http://server.test:345'
cnf.instance_connection_is_secure = False
cnf.instance_connection_https_validate_certificates = False
self.m.ReplayAll()
data = nova_utils.build_userdata(resource,
instance_user=self.instance_user)
data = nova_utils.build_userdata(resource)
self.assertIn("Content-Type: text/cloud-config;", data)
self.assertIn("Content-Type: text/cloud-boothook;", data)
self.assertIn("Content-Type: text/part-handler;", data)
@ -198,7 +185,38 @@ class NovaUtilsUserdataTests(HeatTestCase):
self.assertIn("http://server.test:345", data)
self.assertIn("http://server.test:123", data)
self.assertIn("[Boto]", data)
self.assertIn(self.expect, data)
self.m.VerifyAll()
def test_build_userdata_without_instance_user(self):
"""Don't add a custom instance user when not requested."""
resource = self.m.CreateMockAnything()
resource.metadata = {}
self.m.StubOutWithMock(nova_utils.cfg, 'CONF')
cnf = nova_utils.cfg.CONF
cnf.instance_user = 'config_instance_user'
cnf.heat_metadata_server_url = 'http://server.test:123'
cnf.heat_watch_server_url = 'http://server.test:345'
self.m.ReplayAll()
data = nova_utils.build_userdata(resource, instance_user=None)
self.assertNotIn('user: ', data)
self.assertNotIn('useradd', data)
self.assertNotIn('config_instance_user', data)
self.m.VerifyAll()
def test_build_userdata_with_instance_user(self):
"""Add the custom instance user when requested."""
resource = self.m.CreateMockAnything()
resource.metadata = {}
self.m.StubOutWithMock(nova_utils.cfg, 'CONF')
cnf = nova_utils.cfg.CONF
cnf.instance_user = 'config_instance_user'
cnf.heat_metadata_server_url = 'http://server.test:123'
cnf.heat_watch_server_url = 'http://server.test:345'
self.m.ReplayAll()
data = nova_utils.build_userdata(resource,
instance_user="custominstanceuser")
self.assertNotIn('config_instance_user', data)
self.assertIn("custominstanceuser", data)
self.m.VerifyAll()

@ -1858,7 +1858,100 @@ class ServersTest(HeatTestCase):
self.m.ReplayAll()
self.assertEqual(server._resolve_attribute("accessIPv4"), '')
self.m.VerifyAll()
def test_default_instance_user(self):
"""The default value for instance_user in heat.conf is ec2-user."""
return_server = self.fc.servers.list()[1]
server = self._setup_test_server(return_server, 'default_user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(server,
'wordpress',
instance_user='ec2-user',
user_data_format='HEAT_CFNTOOLS')
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()
def test_admin_user_property(self):
"""Test the admin_user property on the server overrides instance_user.
Launching the instance should call build_userdata with the
custom user name. This property is deprecated and will be
removed in Juno.
"""
return_server = self.fc.servers.list()[1]
stack_name = 'stack_with_custom_admin_user_server'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['admin_user'] = 'custom_user'
server = servers.Server('create_metadata_test_server',
t['Resources']['WebServer'], stack)
server.t = server.stack.resolve_runtime_data(server.t)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=mox.IgnoreArg(), flavor=mox.IgnoreArg(), key_name='test',
name=mox.IgnoreArg(), security_groups=[],
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=mox.IgnoreArg(), nics=None, availability_zone=None,
block_device_mapping=None, config_drive=None,
disk_config=None, reservation_id=None, files={},
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(server,
'wordpress',
instance_user='custom_user',
user_data_format='HEAT_CFNTOOLS')
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()
def test_custom_instance_user(self):
"""Test instance_user in heat.conf being set to a custom value.
Launching the instance should call build_userdata with the
custom user name.
This option is deprecated and will be removed in Juno.
"""
return_server = self.fc.servers.list()[1]
server = self._setup_test_server(return_server, 'custom_user')
self.m.StubOutWithMock(servers.cfg.CONF, 'instance_user')
servers.cfg.CONF.instance_user = 'custom_user'
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(server,
'wordpress',
instance_user='custom_user',
user_data_format='HEAT_CFNTOOLS')
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()
def test_empty_instance_user(self):
"""Test instance_user in heat.conf being empty.
Launching the instance should not pass any user to
build_userdata. The default cloud-init user set up for the image
will be used instead.
This will the default behaviour in Juno once we remove the
instance_user option.
"""
return_server = self.fc.servers.list()[1]
server = self._setup_test_server(return_server, 'custom_user')
self.m.StubOutWithMock(servers.cfg.CONF, 'instance_user')
servers.cfg.CONF.instance_user = ''
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(server,
'wordpress',
instance_user=None,
user_data_format='HEAT_CFNTOOLS')
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()

@ -151,11 +151,13 @@ class ServerTagsTest(HeatTestCase):
# need to resolve the template functions
server_userdata = nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData'])
instance.t['Properties']['UserData'],
'ec2-user')
self.m.StubOutWithMock(nova_utils, 'build_userdata')
nova_utils.build_userdata(
instance,
instance.t['Properties']['UserData']).AndReturn(server_userdata)
instance.t['Properties']['UserData'],
'ec2-user').AndReturn(server_userdata)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(

Loading…
Cancel
Save