re-work vendor-data and smartos
This reduces how much cloud-init is explicitly involved in what "vendor-data" could accomplish. The goal of vendor-data was to provide the vendor with a channel to run arbitrary code that accomodate for their specific platform. Much of those accomodations are currently being done in cloud-init. However, this now moves some of those things to default "vendor-data", instead of cloud-init proper. Basically, now we have an 'sdc:vendor-data' key in the metadata. If that does not exist, then cloud-init will use the default. The default, provides a boothook. That boothook writes a file into /var/lib/cloud/per-boot/ . That file will be both written on every boot and then executed at rc.local time frame (by 'scripts-per-boot'). It will then execute /var/lib/cloud/instance/data/user-script and /var/lib/cloud/instance/data/operator-script if they exist. So, the things that cloud-init is now doing outside of the default vendor-data that I would rather be done in vendor-data is: * managing the population of instance/data/user-script and instance/data/operator-script. These could very easily be done from the boothook, but doing them in cloud-init removes the necessity for having a 'mdata-get' command in the image (or some other way for the boothook script to query the datasource). * managing the LEGACY things.
This commit is contained in:
parent
f528dbf12e
commit
7cd8362c5e
@ -50,7 +50,8 @@ SMARTOS_ATTRIB_MAP = {
|
|||||||
'iptables_disable': ('iptables_disable', True),
|
'iptables_disable': ('iptables_disable', True),
|
||||||
'motd_sys_info': ('motd_sys_info', True),
|
'motd_sys_info': ('motd_sys_info', True),
|
||||||
'availability_zone': ('sdc:datacenter_name', True),
|
'availability_zone': ('sdc:datacenter_name', True),
|
||||||
'vendordata': ('sdc:operator-script', False),
|
'vendor-data': ('sdc:vendor-data', False),
|
||||||
|
'operator-script': ('sdc:operator-script', False),
|
||||||
}
|
}
|
||||||
|
|
||||||
DS_NAME = 'SmartOS'
|
DS_NAME = 'SmartOS'
|
||||||
@ -95,33 +96,45 @@ BUILTIN_CLOUD_CONFIG = {
|
|||||||
'device': 'ephemeral0'}],
|
'device': 'ephemeral0'}],
|
||||||
}
|
}
|
||||||
|
|
||||||
BUILTIN_VENDOR_DATA = """
|
## builtin vendor-data is a boothook that writes a script into
|
||||||
#cloud-config:
|
## /var/lib/cloud/scripts/per-boot. *That* script then handles
|
||||||
write_files:
|
## executing the 'operator-script' and 'user-script' files
|
||||||
- encoding: b64
|
## that cloud-init writes into /var/lib/cloud/instance/data/
|
||||||
owner: root:root
|
## if they exist.
|
||||||
path: %(script_d)s/01_sdc-operator-script.sh
|
##
|
||||||
permissions: '0755'
|
## This is all very indirect, but its done like this so that at
|
||||||
content: |
|
## some point in the future, perhaps cloud-init wouldn't do it at
|
||||||
""" + base64.b64encode("""#!/bin/sh
|
## all, but rather the vendor actually provide vendor-data that accomplished
|
||||||
# This file is written as part of the default vendor data for
|
## their desires. (That is the point of vendor-data).
|
||||||
# SmartOS. This script looks for the SmartDC operator script
|
##
|
||||||
# and then executes it. It will be run each boot.
|
## cloud-init does cheat a bit, and write the operator-script and user-script
|
||||||
|
## itself. It could have the vendor-script do that, but it seems better
|
||||||
|
## to not require the image to contain a tool (mdata-get) to read those
|
||||||
|
## keys when we have a perfectly good one inside cloud-init.
|
||||||
|
BUILTIN_VENDOR_DATA = """\
|
||||||
|
#cloud-boothook
|
||||||
|
#!/bin/sh
|
||||||
|
fname="%(per_boot_d)s/01_smartos_vendor_data.sh"
|
||||||
|
mkdir -p "${fname%%/*}"
|
||||||
|
cat > "$fname" <<"END_SCRIPT"
|
||||||
|
#!/bin/sh
|
||||||
|
##
|
||||||
|
# This file is written as part of the default vendor data for SmartOS.
|
||||||
|
# The SmartOS datasource writes the listed file from the listed metadata key
|
||||||
|
# sdc:operator-script -> %(operator_script)s
|
||||||
|
# user-script -> %(user_script)s
|
||||||
#
|
#
|
||||||
# This requires the Joyent Metadata client to be installed.
|
# You can view content with 'mdata-get <key>'
|
||||||
# On Ubuntu, it is provided via the joyent-mdata-client package
|
#
|
||||||
# Or you can get it via https://github.com/joyent/mdata-client
|
for script in "%(operator_script)s" "%(user_script)s"; do
|
||||||
|
[ -x "$script" ] || continue
|
||||||
|
echo "executing '$script'" 1>&2
|
||||||
|
"$script"
|
||||||
|
done
|
||||||
|
END_SCRIPT
|
||||||
|
chmod +x "$fname"
|
||||||
|
"""
|
||||||
|
|
||||||
my_path=$(dirname $0)
|
|
||||||
[ -x /usr/sbin/mdata-get ] || exit 1
|
|
||||||
|
|
||||||
/usr/sbin/mdata-get sdc:operator-script > \
|
|
||||||
$my_path/operator-script || exit 0
|
|
||||||
|
|
||||||
[ -e $my_path/operator-script ] || exit 0
|
|
||||||
chmod 0700 $my_path/operator-script
|
|
||||||
exec /run/sdc/operator-script
|
|
||||||
""")
|
|
||||||
|
|
||||||
# @datadictionary: this is legacy path for placing files from metadata
|
# @datadictionary: this is legacy path for placing files from metadata
|
||||||
# per the SmartOS location. It is not preferable, but is done for
|
# per the SmartOS location. It is not preferable, but is done for
|
||||||
@ -148,8 +161,7 @@ class DataSourceSmartOS(sources.DataSource):
|
|||||||
self.b64_keys = self.ds_cfg.get('base64_keys')
|
self.b64_keys = self.ds_cfg.get('base64_keys')
|
||||||
self.b64_all = self.ds_cfg.get('base64_all')
|
self.b64_all = self.ds_cfg.get('base64_all')
|
||||||
self.script_base_d = os.path.join(self.paths.get_cpath("scripts"))
|
self.script_base_d = os.path.join(self.paths.get_cpath("scripts"))
|
||||||
self.user_script_d = os.path.join(self.paths.get_cpath("scripts"),
|
self.data_d = os.path.join(self.paths.instance_link, 'data')
|
||||||
'per-boot')
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
root = sources.DataSource.__str__(self)
|
root = sources.DataSource.__str__(self)
|
||||||
@ -168,7 +180,7 @@ class DataSourceSmartOS(sources.DataSource):
|
|||||||
LOG.debug("No dmidata utility found")
|
LOG.debug("No dmidata utility found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
system_uuid, system_type = dmi_info
|
system_uuid, system_type = tuple(dmi_info)
|
||||||
if 'smartdc' not in system_type.lower():
|
if 'smartdc' not in system_type.lower():
|
||||||
LOG.debug("Host is not on SmartOS. system_type=%s", system_type)
|
LOG.debug("Host is not on SmartOS. system_type=%s", system_type)
|
||||||
return False
|
return False
|
||||||
@ -191,11 +203,18 @@ class DataSourceSmartOS(sources.DataSource):
|
|||||||
# to a file in the filesystem of the guest on each boot and then
|
# to a file in the filesystem of the guest on each boot and then
|
||||||
# executed. It may be of any format that would be considered
|
# executed. It may be of any format that would be considered
|
||||||
# executable in the guest instance.
|
# executable in the guest instance.
|
||||||
u_script = md.get('user-script')
|
#
|
||||||
u_script_f = "%s/99_user_script" % self.user_script_d
|
# We write 'user-script' and 'operator-script' into the
|
||||||
|
# instance/data directory. The default vendor-data then handles
|
||||||
|
# executing them later.
|
||||||
|
user_script = os.path.join(self.data_d, 'user-script')
|
||||||
u_script_l = "%s/user-script" % LEGACY_USER_D
|
u_script_l = "%s/user-script" % LEGACY_USER_D
|
||||||
write_boot_content(u_script, u_script_f, link=u_script_l, shebang=True,
|
write_boot_content(md.get('user-script'), content_f=user_script,
|
||||||
mode=0700)
|
link=u_script_l, shebang=True, mode=0700)
|
||||||
|
|
||||||
|
operator_script = os.path.join(self.data_d, 'operator-script')
|
||||||
|
write_boot_content(md.get('operator-script'),
|
||||||
|
content_f=operator_script, shebang=False, mode=0700)
|
||||||
|
|
||||||
# @datadictionary: This key has no defined format, but its value
|
# @datadictionary: This key has no defined format, but its value
|
||||||
# is written to the file /var/db/mdata-user-data on each boot prior
|
# is written to the file /var/db/mdata-user-data on each boot prior
|
||||||
@ -214,14 +233,16 @@ class DataSourceSmartOS(sources.DataSource):
|
|||||||
if md['user-data']:
|
if md['user-data']:
|
||||||
ud = md['user-data']
|
ud = md['user-data']
|
||||||
|
|
||||||
if not md['vendordata']:
|
if not md['vendor-data']:
|
||||||
md['vendordata'] = BUILTIN_VENDOR_DATA % {
|
md['vendor-data'] = BUILTIN_VENDOR_DATA % {
|
||||||
'script_d': self.user_script_d
|
'user_script': user_script,
|
||||||
}
|
'operator_script': operator_script,
|
||||||
|
'per_boot_d': os.path.join(self.paths.get_cpath("scripts"), 'per-boot'),
|
||||||
|
}
|
||||||
|
|
||||||
self.metadata = util.mergemanydict([md, self.metadata])
|
self.metadata = util.mergemanydict([md, self.metadata])
|
||||||
self.userdata_raw = ud
|
self.userdata_raw = ud
|
||||||
self.vendordata_raw = md['vendordata']
|
self.vendordata_raw = md['vendor-data']
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def device_name_to_device(self, name):
|
def device_name_to_device(self, name):
|
||||||
|
@ -44,6 +44,7 @@ MOCK_RETURNS = {
|
|||||||
'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']),
|
'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']),
|
||||||
'sdc:datacenter_name': 'somewhere2',
|
'sdc:datacenter_name': 'somewhere2',
|
||||||
'sdc:operator-script': '\n'.join(['bin/true', '']),
|
'sdc:operator-script': '\n'.join(['bin/true', '']),
|
||||||
|
'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']),
|
||||||
'user-data': '\n'.join(['something', '']),
|
'user-data': '\n'.join(['something', '']),
|
||||||
'user-script': '\n'.join(['/bin/true', '']),
|
'user-script': '\n'.join(['/bin/true', '']),
|
||||||
}
|
}
|
||||||
@ -107,7 +108,6 @@ class MockSerial(object):
|
|||||||
yield '\n'
|
yield '\n'
|
||||||
|
|
||||||
|
|
||||||
#class TestSmartOSDataSource(MockerTestCase):
|
|
||||||
class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
|
class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
helpers.FilesystemMockingTestCase.setUp(self)
|
helpers.FilesystemMockingTestCase.setUp(self)
|
||||||
@ -345,10 +345,10 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
|
|||||||
there is no script remaining.
|
there is no script remaining.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
script_d = os.path.join(self.tmp, "scripts", "per-boot")
|
script_d = os.path.join(self.tmp, "instance", "data")
|
||||||
os.makedirs(script_d)
|
os.makedirs(script_d)
|
||||||
|
|
||||||
test_script_f = "%s/99_user_script" % script_d
|
test_script_f = os.path.join(script_d, 'user-script')
|
||||||
with open(test_script_f, 'w') as f:
|
with open(test_script_f, 'w') as f:
|
||||||
f.write("TEST DATA")
|
f.write("TEST DATA")
|
||||||
|
|
||||||
@ -397,49 +397,20 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
|
|||||||
dsrc = self._get_ds(mockdata=MOCK_RETURNS)
|
dsrc = self._get_ds(mockdata=MOCK_RETURNS)
|
||||||
ret = dsrc.get_data()
|
ret = dsrc.get_data()
|
||||||
self.assertTrue(ret)
|
self.assertTrue(ret)
|
||||||
self.assertEquals(MOCK_RETURNS['sdc:operator-script'],
|
self.assertEquals(MOCK_RETURNS['sdc:vendor-data'],
|
||||||
dsrc.metadata['vendordata'])
|
dsrc.metadata['vendor-data'])
|
||||||
|
|
||||||
def test_default_vendor_data(self):
|
def test_default_vendor_data(self):
|
||||||
my_returns = MOCK_RETURNS.copy()
|
my_returns = MOCK_RETURNS.copy()
|
||||||
def_op_script = my_returns['sdc:operator-script']
|
def_op_script = my_returns['sdc:vendor-data']
|
||||||
del my_returns['sdc:operator-script']
|
del my_returns['sdc:vendor-data']
|
||||||
dsrc = self._get_ds(mockdata=my_returns)
|
dsrc = self._get_ds(mockdata=my_returns)
|
||||||
ret = dsrc.get_data()
|
ret = dsrc.get_data()
|
||||||
self.assertTrue(ret)
|
self.assertTrue(ret)
|
||||||
self.assertNotEquals(def_op_script, dsrc.metadata['vendordata'])
|
self.assertNotEquals(def_op_script, dsrc.metadata['vendor-data'])
|
||||||
|
|
||||||
self.replicateTestRoot('simple_ubuntu', self.tmp)
|
# we expect default vendor-data is a boothook
|
||||||
cfg = {
|
self.assertTrue(dsrc.vendordata_raw.startswith("#cloud-boothook"))
|
||||||
'cloud_init_modules': ['write-files'],
|
|
||||||
}
|
|
||||||
cloud_cfg = util.yaml_dumps(cfg)
|
|
||||||
util.ensure_dir(os.path.join(self.tmp, 'etc', 'cloud'))
|
|
||||||
util.write_file(os.path.join(self.tmp, 'etc',
|
|
||||||
'cloud', 'cloud.cfg'), cloud_cfg)
|
|
||||||
|
|
||||||
self._patchIn(self.tmp)
|
|
||||||
|
|
||||||
initer = stages.Init()
|
|
||||||
initer.read_cfg()
|
|
||||||
initer.datasource = dsrc
|
|
||||||
initer.initialize()
|
|
||||||
initer.fetch()
|
|
||||||
_iid = initer.instancify()
|
|
||||||
initer.update()
|
|
||||||
initer.cloudify().run('consume_data',
|
|
||||||
initer.consume_data,
|
|
||||||
args=[PER_INSTANCE],
|
|
||||||
freq=PER_INSTANCE)
|
|
||||||
mods = stages.Modules(initer)
|
|
||||||
(_which_ran, _failures) = mods.run_section('cloud_init_modules')
|
|
||||||
pb_script_fns = os.path.join(dsrc.paths.get_cpath('scripts'),
|
|
||||||
'per-boot', '01_sdc-operator-script.sh')
|
|
||||||
self.assertTrue(os.path.isfile(pb_script_fns))
|
|
||||||
self.assertTrue(os.access(pb_script_fns, os.X_OK))
|
|
||||||
|
|
||||||
with open(pb_script_fns, 'r') as fd:
|
|
||||||
self.assertIn("#!/bin/sh", fd.readlines()[0])
|
|
||||||
|
|
||||||
def test_disable_iptables_flag(self):
|
def test_disable_iptables_flag(self):
|
||||||
dsrc = self._get_ds(mockdata=MOCK_RETURNS)
|
dsrc = self._get_ds(mockdata=MOCK_RETURNS)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user