Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 18.222.161.245
# Copyright (C) 2012 Canonical Ltd.
# Copyright (C) 2012 Yahoo! Inc.
# Copyright (C) 2012-2013 CERIT Scientific Cloud
# Copyright (C) 2012-2013 OpenNebula.org
# Copyright (C) 2014 Consejo Superior de Investigaciones Cientificas
#
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
# Author: Vlastimil Holer <xholer@mail.muni.cz>
# Author: Javier Fontan <jfontan@opennebula.org>
# Author: Enol Fernandez <enolfc@ifca.unican.es>
#
# This file is part of cloud-init. See LICENSE file for license information.
import collections
import functools
import logging
import os
import pwd
import re
import shlex
import textwrap
from cloudinit import atomic_helper, net, sources, subp, util
LOG = logging.getLogger(__name__)
DEFAULT_IID = "iid-dsopennebula"
DEFAULT_PARSEUSER = "nobody"
CONTEXT_DISK_FILES = ["context.sh"]
EXCLUDED_VARS = (
"EPOCHREALTIME",
"EPOCHSECONDS",
"RANDOM",
"LINENO",
"SECONDS",
"_",
"SRANDOM",
"__v",
)
class DataSourceOpenNebula(sources.DataSource):
dsname = "OpenNebula"
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
self.seed_dir = os.path.join(paths.seed_dir, "opennebula")
self.network = None
def __str__(self):
root = sources.DataSource.__str__(self)
return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
def _get_data(self):
defaults = {"instance-id": DEFAULT_IID}
results = None
seed = None
# decide parseuser for context.sh shell reader
parseuser = DEFAULT_PARSEUSER
if "parseuser" in self.ds_cfg:
parseuser = self.ds_cfg.get("parseuser")
candidates = [self.seed_dir]
candidates.extend(find_candidate_devs())
for cdev in candidates:
try:
if os.path.isdir(self.seed_dir):
results = read_context_disk_dir(
cdev, self.distro, asuser=parseuser
)
elif cdev.startswith("/dev"):
# util.mount_cb only handles passing a single argument
# through to the wrapped function, so we have to partially
# apply the function to pass in `distro`. See LP: #1884979
partially_applied_func = functools.partial(
read_context_disk_dir,
asuser=parseuser,
distro=self.distro,
)
results = util.mount_cb(cdev, partially_applied_func)
except NonContextDiskDir:
continue
except BrokenContextDiskDir as exc:
raise exc
except util.MountFailedError:
LOG.warning("%s was not mountable", cdev)
if results:
seed = cdev
LOG.debug("found datasource in %s", cdev)
break
if not seed:
return False
# merge fetched metadata with datasource defaults
md = results["metadata"]
md = util.mergemanydict([md, defaults])
# check for valid user specified dsmode
self.dsmode = self._determine_dsmode(
[results.get("DSMODE"), self.ds_cfg.get("dsmode")]
)
if self.dsmode == sources.DSMODE_DISABLED:
return False
self.seed = seed
self.network = results.get("network-interfaces")
self.metadata = md
self.userdata_raw = results.get("userdata")
return True
def _get_subplatform(self):
"""Return the subplatform metadata source details."""
if self.seed_dir in self.seed:
subplatform_type = "seed-dir"
else:
subplatform_type = "config-disk"
return "%s (%s)" % (subplatform_type, self.seed)
@property
def network_config(self):
if self.network is not None:
return self.network
else:
return None
def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False):
if resolve_ip is None:
if self.dsmode == sources.DSMODE_NETWORK:
resolve_ip = True
else:
resolve_ip = False
return sources.DataSource.get_hostname(self, fqdn, resolve_ip)
class NonContextDiskDir(Exception):
pass
class BrokenContextDiskDir(Exception):
pass
class OpenNebulaNetwork:
def __init__(self, context, distro, system_nics_by_mac=None):
self.context = context
if system_nics_by_mac is None:
system_nics_by_mac = get_physical_nics_by_mac(distro)
self.ifaces = collections.OrderedDict(
[
k
for k in sorted(
system_nics_by_mac.items(),
key=lambda k: net.natural_sort_key(k[1]),
)
]
)
# OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
# context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
self.context_devname = {}
for k, v in context.items():
m = re.match(r"^(.+)_MAC$", k)
if m:
self.context_devname[v.lower()] = m.group(1)
def mac2ip(self, mac):
return ".".join([str(int(c, 16)) for c in mac.split(":")[2:]])
def get_nameservers(self, dev):
nameservers = {}
dns = self.get_field(dev, "dns", "").split()
dns.extend(self.context.get("DNS", "").split())
if dns:
nameservers["addresses"] = dns
search_domain = self.get_field(dev, "search_domain", "").split()
if search_domain:
nameservers["search"] = search_domain
return nameservers
def get_mtu(self, dev):
return self.get_field(dev, "mtu")
def get_ip(self, dev, mac):
return self.get_field(dev, "ip", self.mac2ip(mac))
def get_ip6(self, dev):
addresses6 = []
ip6 = self.get_field(dev, "ip6")
if ip6:
addresses6.append(ip6)
ip6_ula = self.get_field(dev, "ip6_ula")
if ip6_ula:
addresses6.append(ip6_ula)
return addresses6
def get_ip6_prefix(self, dev):
return self.get_field(dev, "ip6_prefix_length", "64")
def get_gateway(self, dev):
return self.get_field(dev, "gateway")
def get_gateway6(self, dev):
# OpenNebula 6.1.80 introduced new context parameter ETHx_IP6_GATEWAY
# to replace old ETHx_GATEWAY6. Old ETHx_GATEWAY6 will be removed in
# OpenNebula 6.4.0 (https://github.com/OpenNebula/one/issues/5536).
return self.get_field(
dev, "ip6_gateway", self.get_field(dev, "gateway6")
)
def get_mask(self, dev):
return self.get_field(dev, "mask", "255.255.255.0")
def get_field(self, dev, name, default=None):
"""return the field name in context for device dev.
context stores <dev>_<NAME> (example: eth0_DOMAIN).
an empty string for value will return default."""
val = self.context.get(
"_".join(
(
dev,
name,
)
).upper()
)
# allow empty string to return the default.
return default if val in (None, "") else val
def gen_conf(self):
netconf = {}
netconf["version"] = 2
netconf["ethernets"] = {}
ethernets = {}
for mac, dev in self.ifaces.items():
mac = mac.lower()
# c_dev stores name in context 'ETHX' for this device.
# dev stores the current system name.
c_dev = self.context_devname.get(mac, dev)
devconf = {}
# Set MAC address
devconf["match"] = {"macaddress": mac}
# Set IPv4 address
devconf["addresses"] = []
mask = self.get_mask(c_dev)
prefix = str(net.ipv4_mask_to_net_prefix(mask))
devconf["addresses"].append(self.get_ip(c_dev, mac) + "/" + prefix)
# Set IPv6 Global and ULA address
addresses6 = self.get_ip6(c_dev)
if addresses6:
prefix6 = self.get_ip6_prefix(c_dev)
devconf["addresses"].extend(
[i + "/" + prefix6 for i in addresses6]
)
# Set IPv4 default gateway
gateway = self.get_gateway(c_dev)
if gateway:
devconf["gateway4"] = gateway
# Set IPv6 default gateway
gateway6 = self.get_gateway6(c_dev)
if gateway6:
devconf["gateway6"] = gateway6
# Set DNS servers and search domains
nameservers = self.get_nameservers(c_dev)
if nameservers:
devconf["nameservers"] = nameservers
# Set MTU size
mtu = self.get_mtu(c_dev)
if mtu:
devconf["mtu"] = mtu
ethernets[dev] = devconf
netconf["ethernets"] = ethernets
return netconf
def find_candidate_devs():
"""
Return a list of devices that may contain the context disk.
"""
combined = []
for f in ("LABEL=CONTEXT", "LABEL=CDROM", "TYPE=iso9660"):
devs = util.find_devs_with(f)
devs.sort()
for d in devs:
if d not in combined:
combined.append(d)
return combined
def switch_user_cmd(user):
return ["sudo", "-u", user]
def varprinter():
"""print the shell environment variables within delimiters to be parsed"""
return textwrap.dedent(
"""
printf "%s\\0" _start_
[ $0 != 'sh' ] && set -o posix
set
[ $0 != 'sh' ] && set +o posix
printf "%s\\0" _start_
"""
)
def parse_shell_config(content, asuser=None):
"""run content and return environment variables which changed
WARNING: the special variable _start_ is used to delimit content
a context.sh that defines this variable might break in unexpected
ways
compatible with posix shells such as dash and ash and any shell
which supports `set -o posix`
"""
if b"_start_\x00" in content.encode():
LOG.warning(
"User defined _start_ variable in context.sh, this may break"
"cloud-init in unexpected ways."
)
# the rendered 'bcmd' does:
#
# setup: declare variables we use (so they show up in 'all')
# varprinter(allvars): print all variables known at beginning
# content: execute the provided content
# varprinter(keylist): print all variables known after content
#
# output is then a newline terminated array of:
# [0] unwanted content before first _start_
# [1] key=value (for each preset variable)
# [2] unwanted content between second and third _start_
# [3] key=value (for each post set variable)
bcmd = (
varprinter()
+ "{\n%s\n\n:\n} > /dev/null\n" % content
+ varprinter()
+ "\n"
)
cmd = []
if asuser is not None:
cmd = switch_user_cmd(asuser)
cmd.extend(["sh", "-e"])
output = subp.subp(cmd, data=bcmd).stdout
# exclude vars that change on their own or that we used
ret = {}
# Add to ret only things were changed and not in excluded.
# skip all content before initial _start_\x00 pair
sections = output.split("_start_\x00")[1:]
# store env variables prior to content run
# skip all content before second _start\x00 pair
# store env variables prior to content run
before, after = sections[0], sections[2]
pre_env = dict(
variable.split("=", maxsplit=1) for variable in shlex.split(before)
)
post_env = dict(
variable.split("=", maxsplit=1) for variable in shlex.split(after)
)
for key in set(pre_env.keys()).union(set(post_env.keys())):
if key in EXCLUDED_VARS:
continue
value = post_env.get(key)
if value is not None and value != pre_env.get(key):
ret[key] = value
return ret
def read_context_disk_dir(source_dir, distro, asuser=None):
"""
read_context_disk_dir(source_dir):
read source_dir and return a tuple with metadata dict and user-data
string populated. If not a valid dir, raise a NonContextDiskDir
"""
found = {}
for af in CONTEXT_DISK_FILES:
fn = os.path.join(source_dir, af)
if os.path.isfile(fn):
found[af] = fn
if not found:
raise NonContextDiskDir("%s: %s" % (source_dir, "no files found"))
context = {}
results = {"userdata": None, "metadata": {}}
if "context.sh" in found:
if asuser is not None:
try:
pwd.getpwnam(asuser)
except KeyError as e:
raise BrokenContextDiskDir(
"configured user '{user}' does not exist".format(
user=asuser
)
) from e
try:
path = os.path.join(source_dir, "context.sh")
content = util.load_text_file(path)
context = parse_shell_config(content, asuser=asuser)
except subp.ProcessExecutionError as e:
raise BrokenContextDiskDir(
"Error processing context.sh: %s" % (e)
) from e
except IOError as e:
raise NonContextDiskDir(
"Error reading context.sh: %s" % (e)
) from e
else:
raise NonContextDiskDir("Missing context.sh")
if not context:
return results
results["metadata"] = context
# process single or multiple SSH keys
ssh_key_var = None
if "SSH_KEY" in context:
ssh_key_var = "SSH_KEY"
elif "SSH_PUBLIC_KEY" in context:
ssh_key_var = "SSH_PUBLIC_KEY"
if ssh_key_var:
lines = context.get(ssh_key_var).splitlines()
results["metadata"]["public-keys"] = [
line for line in lines if len(line) and not line.startswith("#")
]
# custom hostname -- try hostname or leave cloud-init
# itself create hostname from IP address later
for k in ("SET_HOSTNAME", "HOSTNAME", "PUBLIC_IP", "IP_PUBLIC", "ETH0_IP"):
if k in context:
results["metadata"]["local-hostname"] = context[k]
break
# raw user data
if "USER_DATA" in context:
results["userdata"] = context["USER_DATA"]
elif "USERDATA" in context:
results["userdata"] = context["USERDATA"]
# b64decode user data if necessary (default)
if "userdata" in results:
encoding = context.get(
"USERDATA_ENCODING", context.get("USER_DATA_ENCODING")
)
if encoding == "base64":
try:
results["userdata"] = atomic_helper.b64d(results["userdata"])
except TypeError:
LOG.warning("Failed base64 decoding of userdata")
# generate Network Configuration v2
# only if there are any required context variables
# http://docs.opennebula.org/5.4/operation/references/template.html#context-section
ipaddr_keys = [k for k in context if re.match(r"^ETH\d+_IP.*$", k)]
if ipaddr_keys:
onet = OpenNebulaNetwork(context, distro)
results["network-interfaces"] = onet.gen_conf()
return results
def get_physical_nics_by_mac(distro):
devs = net.get_interfaces_by_mac()
return dict(
[(m, n) for m, n in devs.items() if distro.networking.is_physical(n)]
)
# Legacy: Must be present in case we load an old pkl object
DataSourceOpenNebulaNet = DataSourceOpenNebula
# Used to match classes to dependencies
datasources = [
(DataSourceOpenNebula, (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)
|