# Copyright (C) 2018 Warsaw Data Center
#
# Author: Malwina Leis <m.leis@rootbox.com>
# Author: Grzegorz Brzeski <gregory@rootbox.io>
# Author: Adam Dobrawy <a.dobrawy@hyperone.com>
#
# This file is part of cloud-init. See LICENSE file for license information.
"""
This file contains code used to gather the user data passed to an
instance on rootbox / hyperone cloud platforms
"""
import errno
import os
import os.path
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import util
from cloudinit.event import EventType
LOG = logging.getLogger(__name__)
ETC_HOSTS = '/etc/hosts'
def get_manage_etc_hosts():
hosts = util.load_file(ETC_HOSTS, quiet=True)
if hosts:
LOG.debug('/etc/hosts exists - setting manage_etc_hosts to False')
return False
LOG.debug('/etc/hosts does not exists - setting manage_etc_hosts to True')
return True
def ip2int(addr):
parts = addr.split('.')
return (int(parts[0]) << 24) + (int(parts[1]) << 16) + \
(int(parts[2]) << 8) + int(parts[3])
def int2ip(addr):
return '.'.join([str(addr >> (i << 3) & 0xFF) for i in range(4)[::-1]])
def _sub_arp(cmd):
"""
Uses the prefered cloud-init subprocess def of util.subp
and runs arping. Breaking this to a separate function
for later use in mocking and unittests
"""
return util.subp(['arping'] + cmd)
def gratuitous_arp(items, distro):
source_param = '-S'
if distro.name in ['fedora', 'centos', 'rhel']:
source_param = '-s'
for item in items:
_sub_arp([
'-c', '2',
source_param, item['source'],
item['destination']
])
def get_md():
rbx_data = None
devices = [
dev
for dev, bdata in util.blkid().items()
if bdata.get('LABEL', '').upper() == 'CLOUDMD'
]
for device in devices:
try:
rbx_data = util.mount_cb(
device=device,
callback=read_user_data_callback,
mtype=['vfat', 'fat']
)
if rbx_data:
break
except OSError as err:
if err.errno != errno.ENOENT:
raise
except util.MountFailedError:
util.logexc(LOG, "Failed to mount %s when looking for user "
"data", device)
if not rbx_data:
util.logexc(LOG, "Failed to load metadata and userdata")
return False
return rbx_data
def generate_network_config(netadps):
"""Generate network configuration
@param netadps: A list of network adapter settings
@returns: A dict containing network config
"""
return {
'version': 1,
'config': [
{
'type': 'physical',
'name': 'eth{}'.format(str(i)),
'mac_address': netadp['macaddress'].lower(),
'subnets': [
{
'type': 'static',
'address': ip['address'],
'netmask': netadp['network']['netmask'],
'control': 'auto',
'gateway': netadp['network']['gateway'],
'dns_nameservers': netadp['network']['dns'][
'nameservers']
} for ip in netadp['ip']
],
} for i, netadp in enumerate(netadps)
]
}
def read_user_data_callback(mount_dir):
"""This callback will be applied by util.mount_cb() on the mounted
drive.
@param mount_dir: String representing path of directory where mounted drive
is available
@returns: A dict containing userdata, metadata and cfg based on metadata.
"""
meta_data = util.load_json(
text=util.load_file(
fname=os.path.join(mount_dir, 'cloud.json'),
decode=False
)
)
user_data = util.load_file(
fname=os.path.join(mount_dir, 'user.data'),
quiet=True
)
if 'vm' not in meta_data or 'netadp' not in meta_data:
util.logexc(LOG, "Failed to load metadata. Invalid format.")
return None
username = meta_data.get('additionalMetadata', {}).get('username')
ssh_keys = meta_data.get('additionalMetadata', {}).get('sshKeys', [])
hash = None
if meta_data.get('additionalMetadata', {}).get('password'):
hash = meta_data['additionalMetadata']['password']['sha512']
network = generate_network_config(meta_data['netadp'])
data = {
'userdata': user_data,
'metadata': {
'instance-id': meta_data['vm']['_id'],
'local-hostname': meta_data['vm']['name'],
'public-keys': []
},
'gratuitous_arp': [
{
"source": ip["address"],
"destination": target
}
for netadp in meta_data['netadp']
for ip in netadp['ip']
for target in [
netadp['network']["gateway"],
int2ip(ip2int(netadp['network']["gateway"]) + 2),
int2ip(ip2int(netadp['network']["gateway"]) + 3)
]
],
'cfg': {
'ssh_pwauth': True,
'disable_root': True,
'system_info': {
'default_user': {
'name': username,
'gecos': username,
'sudo': ['ALL=(ALL) NOPASSWD:ALL'],
'passwd': hash,
'lock_passwd': False,
'ssh_authorized_keys': ssh_keys,
'shell': '/bin/bash'
}
},
'network_config': network,
'manage_etc_hosts': get_manage_etc_hosts(),
},
}
LOG.debug('returning DATA object:')
LOG.debug(data)
return data
class DataSourceRbxCloud(sources.DataSource):
dsname = "RbxCloud"
update_events = {'network': [
EventType.BOOT_NEW_INSTANCE,
EventType.BOOT
]}
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
def __str__(self):
root = sources.DataSource.__str__(self)
return "%s [seed=%s]" % (root, self.seed)
def _get_data(self):
"""
Metadata is passed to the launching instance which
is used to perform instance configuration.
"""
rbx_data = get_md()
self.userdata_raw = rbx_data['userdata']
self.metadata = rbx_data['metadata']
self.gratuitous_arp = rbx_data['gratuitous_arp']
self.cfg = rbx_data['cfg']
return True
@property
def network_config(self):
return self.cfg['network_config']
def get_public_ssh_keys(self):
return self.metadata['public-keys']
def get_userdata_raw(self):
return self.userdata_raw
def get_config_obj(self):
return self.cfg
def activate(self, cfg, is_new_instance):
gratuitous_arp(self.gratuitous_arp, self.distro)
# Used to match classes to dependencies
datasources = [
(DataSourceRbxCloud, (sources.DEP_FILESYSTEM,)),
]
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)