Index: net/Makefile =================================================================== --- net/Makefile +++ net/Makefile @@ -73,6 +73,7 @@ SUBDIR += citrix_ica SUBDIR += cjdns SUBDIR += cloud-init + SUBDIR += cloud-init-azure SUBDIR += clusterit SUBDIR += cnd SUBDIR += coda6_client Index: net/cloud-init-azure/Makefile =================================================================== --- /dev/null +++ net/cloud-init-azure/Makefile @@ -0,0 +1,49 @@ +# $FreeBSD$ + +PORTNAME= cloud-init +PKGNAMESUFFIX?= -azure +PORTVERSION= 0.7.9 +CATEGORIES= net python +MASTER_SITES= http://launchpad.net/${PORTNAME}/trunk/${PORTVERSION}/+download/ +PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX} + +MAINTAINER= honzhan@microsoft.com +COMMENT= Init scripts for use on cloud images + +LICENSE= GPLv3 +LICENSE_FILE= ${WRKSRC}/LICENSE + +RUN_DEPENDS= dmidecode>0:sysutils/dmidecode \ + e2fsprogs>0:sysutils/e2fsprogs \ + python>0:lang/python \ + ${PYTHON_PKGNAMEPREFIX}boto>0:devel/py-boto \ + ${PYTHON_PKGNAMEPREFIX}Jinja2>0:devel/py-Jinja2 \ + ${PYTHON_PKGNAMEPREFIX}cheetah>0:devel/py-cheetah \ + ${PYTHON_PKGNAMEPREFIX}prettytable>0:devel/py-prettytable \ + ${PYTHON_PKGNAMEPREFIX}configobj>0:devel/py-configobj \ + ${PYTHON_PKGNAMEPREFIX}yaml>0:devel/py-yaml \ + ${PYTHON_PKGNAMEPREFIX}six>0:devel/py-six \ + ${PYTHON_PKGNAMEPREFIX}serial>0:comms/py-serial \ + ${PYTHON_PKGNAMEPREFIX}requests>0:www/py-requests \ + ${PYTHON_PKGNAMEPREFIX}oauthlib>0:security/py-oauthlib \ + ${PYTHON_PKGNAMEPREFIX}jsonpatch>0:devel/py-jsonpatch \ + ${PYTHON_PKGNAMEPREFIX}jsonpointer>0:devel/py-jsonpointer + +ETCDIR= ${PREFIX}/etc/cloud + +USES= python:2.7 shebangfix +SHEBANG_FILES= tools/validate-yaml.py tools/read-dependencies \ + tools/read-version tools/hacking.py +USE_PYTHON= autoplist distutils + +PYDISTUTILS_INSTALLARGS+= "--init-system=sysvinit_freebsd" + +ONLY_FOR_ARCHS= amd64 i386 +ONLY_FOR_ARCHS_REASON= currently depends on dmidecode which is x86-only + +post-patch: + @${FIND} ${WRKSRC} -iname "*.orig" | ${XARGS} ${RM} +post-build: + @cd ${WRKSRC} ; ${MV} config/cloud.cfg-freebsd config/cloud.cfg + +.include Index: net/cloud-init-azure/distinfo =================================================================== --- /dev/null +++ net/cloud-init-azure/distinfo @@ -0,0 +1,2 @@ +SHA256 (cloud-init-0.7.9.tar.gz) = 76edb80bf1bdbda68f8014bc057a303ae438a139bdf394e825e548d6ae39d472 +SIZE (cloud-init-0.7.6.tar.gz) = 602188 Index: net/cloud-init-azure/files/patch-frbsd-azure.txt =================================================================== --- /dev/null +++ net/cloud-init-azure/files/patch-frbsd-azure.txt @@ -0,0 +1,1214 @@ +--- cloudinit/config/cc_resizefs.py.orig ++++ cloudinit/config/cc_resizefs.py +@@ -33,7 +33,10 @@ disabled altogether by setting ``resize_rootfs`` to ``false``. + """ + + import errno ++import getopt + import os ++import re ++import shlex + import stat + + from cloudinit.settings import PER_ALWAYS +@@ -58,6 +61,62 @@ def _resize_ufs(mount_point, devpth): + return ('growfs', devpth) + + ++def _get_dumpfs_output(mount_point): ++ dumpfs_res, err = util.subp(['dumpfs', '-m', mount_point]) ++ return dumpfs_res ++ ++ ++def _get_gpart_output(part): ++ gpart_res, err = util.subp(['gpart', 'show', part]) ++ return gpart_res ++ ++ ++def _can_skip_resize_ufs(mount_point, devpth): ++ # extract the current fs sector size ++ """ ++ # dumpfs -m / ++ # newfs command for / (/dev/label/rootfs) ++ newfs -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 -f 4096 -g 16384 ++ -h 64 -i 8192 -j -k 6408 -m 8 -o time -s 58719232 /dev/label/rootf ++ """ ++ cur_fs_sz = None ++ frag_sz = None ++ dumpfs_res = _get_dumpfs_output(mount_point) ++ for line in dumpfs_res.splitlines(): ++ if not line.startswith('#'): ++ newfs_cmd = shlex.split(line) ++ opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:' ++ optlist, args = getopt.getopt(newfs_cmd[1:], opt_value) ++ for o, a in optlist: ++ if o == "-s": ++ cur_fs_sz = int(a) ++ if o == "-f": ++ frag_sz = int(a) ++ # check the current partition size ++ """ ++ # gpart show /dev/da0 ++=> 40 62914480 da0 GPT (30G) ++ 40 1024 1 freebsd-boot (512K) ++ 1064 58719232 2 freebsd-ufs (28G) ++ 58720296 3145728 3 freebsd-swap (1.5G) ++ 61866024 1048496 - free - (512M) ++ """ ++ expect_sz = None ++ m = re.search('^(/dev/.+)p([0-9])$', devpth) ++ gpart_res = _get_gpart_output(m.group(1)) ++ for line in gpart_res.splitlines(): ++ if re.search(r"freebsd-ufs", line): ++ fields = line.split() ++ expect_sz = int(fields[1]) ++ # Normalize the gpart sector size, ++ # because the size is not exactly the same as fs size. ++ normal_expect_sz = (expect_sz - expect_sz % (frag_sz / 512)) ++ if normal_expect_sz == cur_fs_sz: ++ return True ++ else: ++ return False ++ ++ + # Do not use a dictionary as these commands should be able to be used + # for multiple filesystem types if possible, e.g. one command for + # ext2, ext3 and ext4. +@@ -68,6 +127,10 @@ RESIZE_FS_PREFIXES_CMDS = [ + ('ufs', _resize_ufs), + ] + ++RESIZE_FS_PRECHECK_CMDS = { ++ 'ufs': _can_skip_resize_ufs ++} ++ + NOBLOCK = "noblock" + + +@@ -90,6 +153,14 @@ def rootdev_from_cmdline(cmdline): + return "/dev/" + found + + ++def can_skip_resize(fs_type, resize_what, devpth): ++ fstype_lc = fs_type.lower() ++ for i, func in RESIZE_FS_PRECHECK_CMDS.items(): ++ if fstype_lc.startswith(i): ++ return func(resize_what, devpth) ++ return False ++ ++ + def handle(name, cfg, _cloud, log, args): + if len(args) != 0: + resize_root = args[0] +@@ -158,6 +229,11 @@ def handle(name, cfg, _cloud, log, args): + return + + resizer = None ++ if can_skip_resize(fs_type, resize_what, devpth): ++ log.debug("Skip resize filesystem type %s for %s", ++ fs_type, resize_what) ++ return ++ + fstype_lc = fs_type.lower() + for (pfix, root_cmd) in RESIZE_FS_PREFIXES_CMDS: + if fstype_lc.startswith(pfix): +--- cloudinit/distros/__init__.py.orig ++++ cloudinit/distros/__init__.py +@@ -142,6 +142,9 @@ class Distro(object): + ns, header=header, render_hwaddress=True) + return self.apply_network(contents, bring_up=bring_up) + ++ def generate_fallback_config(self): ++ return net.generate_fallback_config() ++ + def apply_network_config(self, netconfig, bring_up=False): + # apply network config netconfig + # This method is preferred to apply_network which only takes +--- cloudinit/distros/freebsd.py.orig ++++ cloudinit/distros/freebsd.py +@@ -30,6 +30,7 @@ class Distro(distros.Distro): + login_conf_fn_bak = '/etc/login.conf.orig' + resolv_conf_fn = '/etc/resolv.conf' + ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users' ++ default_primary_nic = 'hn0' + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) +@@ -38,6 +39,8 @@ class Distro(distros.Distro): + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + self.osfamily = 'freebsd' ++ self.ipv4_pat = re.compile(r"\s+inet\s+\d+[.]\d+[.]\d+[.]\d+") ++ cfg['ssh_svcname'] = 'sshd' + + # Updates a key in /etc/rc.conf. + def updatercconf(self, key, value): +@@ -183,7 +186,6 @@ class Distro(distros.Distro): + "gecos": '-c', + "primary_group": '-g', + "groups": '-G', +- "passwd": '-h', + "shell": '-s', + "inactive": '-E', + } +@@ -193,19 +195,11 @@ class Distro(distros.Distro): + "no_log_init": '--no-log-init', + } + +- redact_opts = ['passwd'] +- + for key, val in kwargs.items(): + if (key in adduser_opts and val and + isinstance(val, six.string_types)): + adduser_cmd.extend([adduser_opts[key], val]) + +- # Redact certain fields from the logs +- if key in redact_opts: +- log_adduser_cmd.extend([adduser_opts[key], 'REDACTED']) +- else: +- log_adduser_cmd.extend([adduser_opts[key], val]) +- + elif key in adduser_flags and val: + adduser_cmd.append(adduser_flags[key]) + log_adduser_cmd.append(adduser_flags[key]) +@@ -226,19 +220,21 @@ class Distro(distros.Distro): + except Exception as e: + util.logexc(LOG, "Failed to create user %s", name) + raise e ++ # Set the password if it is provided ++ # For security consideration, only hashed passwd is assumed ++ passwd_val = kwargs.get('passwd', None) ++ if passwd_val is not None: ++ self.set_passwd(name, passwd_val, hashed=True) + + def set_passwd(self, user, passwd, hashed=False): +- cmd = ['pw', 'usermod', user] +- + if hashed: +- cmd.append('-H') ++ hash_opt = "-H" + else: +- cmd.append('-h') +- +- cmd.append('0') ++ hash_opt = "-h" + + try: +- util.subp(cmd, passwd, logstring="chpasswd for %s" % user) ++ util.subp(['pw', 'usermod', user, hash_opt, '0'], ++ data=passwd, logstring="chpasswd for %s" % user) + except Exception as e: + util.logexc(LOG, "Failed to set password for %s", user) + raise e +@@ -271,6 +267,255 @@ class Distro(distros.Distro): + keys = set(kwargs['ssh_authorized_keys']) or [] + ssh_util.setup_user_keys(keys, name, options=None) + ++ @staticmethod ++ def get_ifconfig_list(): ++ cmd = ['ifconfig', '-l'] ++ (nics, err) = util.subp(cmd, rcs=[0, 1]) ++ if len(err): ++ LOG.warn("Error running %s: %s", cmd, err) ++ return None ++ return nics ++ ++ @staticmethod ++ def get_ifconfig_ifname_out(ifname): ++ cmd = ['ifconfig', ifname] ++ (if_result, err) = util.subp(cmd, rcs=[0, 1]) ++ if len(err): ++ LOG.warn("Error running %s: %s", cmd, err) ++ return None ++ return if_result ++ ++ @staticmethod ++ def get_ifconfig_ether(): ++ cmd = ['ifconfig', '-l', 'ether'] ++ (nics, err) = util.subp(cmd, rcs=[0, 1]) ++ if len(err): ++ LOG.warn("Error running %s: %s", cmd, err) ++ return None ++ return nics ++ ++ @staticmethod ++ def get_interface_mac(ifname): ++ if_result = Distro.get_ifconfig_ifname_out(ifname) ++ for item in if_result.splitlines(): ++ if item.find('ether ') != -1: ++ mac = str(item.split()[1]) ++ if mac: ++ return mac ++ ++ @staticmethod ++ def get_devicelist(): ++ nics = Distro.get_ifconfig_list() ++ return nics.split() ++ ++ @staticmethod ++ def get_ipv6(): ++ ipv6 = [] ++ nics = Distro.get_devicelist() ++ for nic in nics: ++ if_result = Distro.get_ifconfig_ifname_out(nic) ++ for item in if_result.splitlines(): ++ if item.find("inet6 ") != -1 and item.find("scopeid") == -1: ++ ipv6.append(nic) ++ return ipv6 ++ ++ def get_ipv4(self): ++ ipv4 = [] ++ nics = Distro.get_devicelist() ++ for nic in nics: ++ if_result = Distro.get_ifconfig_ifname_out(nic) ++ for item in if_result.splitlines(): ++ print(item) ++ if self.ipv4_pat.match(item): ++ ipv4.append(nic) ++ return ipv4 ++ ++ def is_up(self, ifname): ++ if_result = Distro.get_ifconfig_ifname_out(ifname) ++ pat = "^" + ifname ++ for item in if_result.splitlines(): ++ if re.match(pat, item): ++ flags = item.split('<')[1].split('>')[0] ++ if flags.find("UP") != -1: ++ return True ++ ++ def _get_current_rename_info(self, check_downable=True): ++ """Collect information necessary for rename_interfaces.""" ++ names = Distro.get_devicelist() ++ bymac = {} ++ for n in names: ++ bymac[Distro.get_interface_mac(n)] = { ++ 'name': n, 'up': self.is_up(n), 'downable': None} ++ ++ if check_downable: ++ nics_with_addresses = set() ++ ipv6 = self.get_ipv6() ++ ipv4 = self.get_ipv4() ++ for bytes_out in (ipv6, ipv4): ++ for i in ipv6: ++ nics_with_addresses.update(i) ++ for i in ipv4: ++ nics_with_addresses.update(i) ++ ++ for d in bymac.values(): ++ d['downable'] = (d['up'] is False or ++ d['name'] not in nics_with_addresses) ++ ++ return bymac ++ ++ def _rename_interfaces(self, renames): ++ if not len(renames): ++ LOG.debug("no interfaces to rename") ++ return ++ ++ current_info = self._get_current_rename_info() ++ ++ cur_bymac = {} ++ for mac, data in current_info.items(): ++ cur = data.copy() ++ cur['mac'] = mac ++ cur_bymac[mac] = cur ++ ++ def update_byname(bymac): ++ return dict((data['name'], data) ++ for data in bymac.values()) ++ ++ def rename(cur, new): ++ util.subp(["ifconfig", cur, "name", new], capture=True) ++ ++ def down(name): ++ util.subp(["ifconfig", name, "down"], capture=True) ++ ++ def up(name): ++ util.subp(["ifconfig", name, "up"], capture=True) ++ ++ ops = [] ++ errors = [] ++ ups = [] ++ cur_byname = update_byname(cur_bymac) ++ tmpname_fmt = "cirename%d" ++ tmpi = -1 ++ ++ for mac, new_name in renames: ++ cur = cur_bymac.get(mac, {}) ++ cur_name = cur.get('name') ++ cur_ops = [] ++ if cur_name == new_name: ++ # nothing to do ++ continue ++ ++ if not cur_name: ++ errors.append("[nic not present] Cannot rename mac=%s to %s" ++ ", not available." % (mac, new_name)) ++ continue ++ ++ if cur['up']: ++ msg = "[busy] Error renaming mac=%s from %s to %s" ++ if not cur['downable']: ++ errors.append(msg % (mac, cur_name, new_name)) ++ continue ++ cur['up'] = False ++ cur_ops.append(("down", mac, new_name, (cur_name,))) ++ ups.append(("up", mac, new_name, (new_name,))) ++ ++ if new_name in cur_byname: ++ target = cur_byname[new_name] ++ if target['up']: ++ msg = "[busy-target] Error renaming mac=%s from %s to %s." ++ if not target['downable']: ++ errors.append(msg % (mac, cur_name, new_name)) ++ continue ++ else: ++ cur_ops.append(("down", mac, new_name, (new_name,))) ++ ++ tmp_name = None ++ while tmp_name is None or tmp_name in cur_byname: ++ tmpi += 1 ++ tmp_name = tmpname_fmt % tmpi ++ ++ cur_ops.append(("rename", mac, new_name, (new_name, tmp_name))) ++ target['name'] = tmp_name ++ cur_byname = update_byname(cur_bymac) ++ if target['up']: ++ ups.append(("up", mac, new_name, (tmp_name,))) ++ ++ cur_ops.append(("rename", mac, new_name, (cur['name'], new_name))) ++ cur['name'] = new_name ++ cur_byname = update_byname(cur_bymac) ++ ops += cur_ops ++ ++ opmap = {'rename': rename, 'down': down, 'up': up} ++ if len(ops) + len(ups) == 0: ++ if len(errors): ++ LOG.debug("unable to do any work for renaming of %s", renames) ++ else: ++ LOG.debug("no work necessary for renaming of %s", renames) ++ else: ++ LOG.debug("achieving renaming of %s with ops %s", ++ renames, ops + ups) ++ ++ for op, mac, new_name, params in ops + ups: ++ try: ++ opmap.get(op)(*params) ++ except Exception as e: ++ errors.append( ++ "[unknown] Error performing %s%s for %s, %s: %s" % ++ (op, params, mac, new_name, e)) ++ if len(errors): ++ raise Exception('\n'.join(errors)) ++ ++ def apply_network_config_names(self, netcfg): ++ renames = [] ++ for ent in netcfg.get('config', {}): ++ if ent.get('type') != 'physical': ++ continue ++ mac = ent.get('mac_address') ++ name = ent.get('name') ++ if not mac: ++ continue ++ renames.append([mac, name]) ++ return self._rename_interfaces(renames) ++ ++ @classmethod ++ def generate_fallback_config(self): ++ nics = Distro.get_ifconfig_ether() ++ if nics is None: ++ LOG.debug("Fail to get network interfaces") ++ return None ++ potential_interfaces = nics.split() ++ connected = [] ++ for nic in potential_interfaces: ++ pat = "^" + nic ++ if_result = Distro.get_ifconfig_ifname_out(nic) ++ for item in if_result.split("\n"): ++ if re.match(pat, item): ++ flags = item.split('<')[1].split('>')[0] ++ if flags.find("RUNNING") != -1: ++ connected.append(nic) ++ if connected: ++ potential_interfaces = connected ++ names = list(sorted(potential_interfaces)) ++ default_pri_nic = Distro.default_primary_nic ++ if default_pri_nic in names: ++ names.remove(default_pri_nic) ++ names.insert(0, default_pri_nic) ++ target_name = None ++ target_mac = None ++ for name in names: ++ mac = Distro.get_interface_mac(name) ++ if mac: ++ target_name = name ++ target_mac = mac ++ break ++ if target_mac and target_name: ++ nconf = {'config': [], 'version': 1} ++ nconf['config'].append( ++ {'type': 'physical', 'name': target_name, ++ 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}) ++ return nconf ++ else: ++ return None ++ + def _write_network(self, settings): + entries = net_util.translate_network(settings) + nameservers = [] +--- cloudinit/settings.py.orig ++++ cloudinit/settings.py +@@ -37,7 +37,7 @@ CFG_BUILTIN = { + ], + 'def_log_file': '/var/log/cloud-init.log', + 'log_cfgs': [], +- 'syslog_fix_perms': ['syslog:adm', 'root:adm'], ++ 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel'], + 'system_info': { + 'paths': { + 'cloud_dir': '/var/lib/cloud', +--- cloudinit/sources/DataSourceAzure.py.orig ++++ cloudinit/sources/DataSourceAzure.py +@@ -10,6 +10,7 @@ import crypt + from functools import partial + import os + import os.path ++import re + import time + from xml.dom import minidom + import xml.etree.ElementTree as ET +@@ -32,19 +33,160 @@ BOUNCE_COMMAND = [ + # azure systems will always have a resource disk, and 66-azure-ephemeral.rules + # ensures that it gets linked to this path. + RESOURCE_DISK_PATH = '/dev/disk/cloud/azure_resource' ++DEFAULT_PRIMARY_NIC = 'eth0' ++LEASE_FILE = '/var/lib/dhcp/dhclient.eth0.leases' ++DEFAULT_FS = 'ext4' ++ ++ ++def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid): ++ # extract the 'X' from dev.storvsc.X. if deviceid matches ++ """ ++ dev.storvsc.1.%pnpinfo: ++ classid=32412632-86cb-44a2-9b5c-50d1417354f5 ++ deviceid=00000000-0001-8899-0000-000000000000 ++ """ ++ for line in sysctl_out.splitlines(): ++ if re.search(r"pnpinfo", line): ++ fields = line.split() ++ if len(fields) >= 3: ++ columns = fields[2].split('=') ++ if (len(columns) >= 2 and ++ columns[0] == "deviceid" and ++ columns[1].startswith(deviceid)): ++ comps = fields[0].split('.') ++ return comps[2] ++ return None ++ ++ ++def find_busdev_from_disk(camcontrol_out, disk_drv): ++ # find the scbusX from 'camcontrol devlist -b' output ++ # if disk_drv matches the specified disk driver, i.e. blkvsc1 ++ """ ++ scbus0 on ata0 bus 0 ++ scbus1 on ata1 bus 0 ++ scbus2 on blkvsc0 bus 0 ++ scbus3 on blkvsc1 bus 0 ++ scbus4 on storvsc2 bus 0 ++ scbus5 on storvsc3 bus 0 ++ scbus-1 on xpt0 bus 0 ++ """ ++ for line in camcontrol_out.splitlines(): ++ if re.search(disk_drv, line): ++ items = line.split() ++ return items[0] ++ return None ++ ++ ++def find_dev_from_busdev(camcontrol_out, busdev): ++ # find the daX from 'camcontrol devlist' output ++ # if busdev matches the specified value, i.e. 'scbus2' ++ """ ++ at scbus1 target 0 lun 0 (cd0,pass0) ++ at scbus2 target 0 lun 0 (da0,pass1) ++ at scbus3 target 1 lun 0 (da1,pass2) ++ """ ++ for line in camcontrol_out.splitlines(): ++ if re.search(busdev, line): ++ items = line.split('(') ++ if len(items) == 2: ++ dev_pass = items[1].split(',') ++ return dev_pass[0] ++ return None ++ ++ ++def get_dev_storvsc_sysctl(): ++ try: ++ sysctl_out, err = util.subp(['sysctl', 'dev.storvsc']) ++ except util.ProcessExecutionError: ++ LOG.debug("Fail to execute sysctl dev.storvsc") ++ return None ++ return sysctl_out ++ ++ ++def get_camcontrol_dev_bus(): ++ try: ++ camcontrol_b_out, err = util.subp(['camcontrol', 'devlist', '-b']) ++ except util.ProcessExecutionError: ++ LOG.debug("Fail to execute camcontrol devlist -b") ++ return None ++ return camcontrol_b_out ++ ++ ++def get_camcontrol_dev(): ++ try: ++ camcontrol_out, err = util.subp(['camcontrol', 'devlist']) ++ except util.ProcessExecutionError: ++ LOG.debug("Fail to execute camcontrol devlist") ++ return None ++ return camcontrol_out ++ ++ ++def get_resource_disk_on_freebsd(port_id): ++ g0 = "00000000" ++ if port_id > 1: ++ g0 = "00000001" ++ port_id = port_id - 2 ++ g1 = "000" + str(port_id) ++ g0g1 = "{0}-{1}".format(g0, g1) ++ """ ++ search 'X' from ++ 'dev.storvsc.X.%pnpinfo: ++ classid=32412632-86cb-44a2-9b5c-50d1417354f5 ++ deviceid=00000000-0001-8899-0000-000000000000' ++ """ ++ sysctl_out = get_dev_storvsc_sysctl() ++ ++ storvscid = find_storvscid_from_sysctl_pnpinfo(sysctl_out, g0g1) ++ if not storvscid: ++ LOG.debug("Fail to find storvsc id from sysctl") ++ return None ++ ++ camcontrol_b_out = get_camcontrol_dev_bus() ++ camcontrol_out = get_camcontrol_dev() ++ # try to find /dev/XX from 'blkvsc' device ++ blkvsc = "blkvsc{0}".format(storvscid) ++ scbusx = find_busdev_from_disk(camcontrol_b_out, blkvsc) ++ if scbusx: ++ devname = find_dev_from_busdev(camcontrol_out, scbusx) ++ if devname is None: ++ LOG.debug("Fail to find /dev/daX") ++ return None ++ return devname ++ # try to find /dev/XX from 'storvsc' device ++ storvsc = "storvsc{0}".format(storvscid) ++ scbusx = find_busdev_from_disk(camcontrol_b_out, storvsc) ++ if scbusx: ++ devname = find_dev_from_busdev(camcontrol_out, scbusx) ++ if devname is None: ++ LOG.debug("Fail to find /dev/daX") ++ return None ++ return devname ++ return None ++ ++# update the FreeBSD specific information ++if util.is_FreeBSD(): ++ DEFAULT_PRIMARY_NIC = 'hn0' ++ LEASE_FILE = '/var/db/dhclient.leases.hn0' ++ DEFAULT_FS = 'freebsd-ufs' ++ res_disk = get_resource_disk_on_freebsd(1) ++ if res_disk is not None: ++ LOG.debug("resource disk is not None") ++ RESOURCE_DISK_PATH = "/dev/" + res_disk ++ else: ++ LOG.debug("resource disk is None") + + BUILTIN_DS_CONFIG = { + 'agent_command': AGENT_START_BUILTIN, + 'data_dir': "/var/lib/waagent", + 'set_hostname': True, + 'hostname_bounce': { +- 'interface': 'eth0', ++ 'interface': DEFAULT_PRIMARY_NIC, + 'policy': True, + 'command': BOUNCE_COMMAND, + 'hostname_command': 'hostname', + }, + 'disk_aliases': {'ephemeral0': RESOURCE_DISK_PATH}, +- 'dhclient_lease_file': '/var/lib/dhcp/dhclient.eth0.leases', ++ 'dhclient_lease_file': LEASE_FILE, + } + + BUILTIN_CLOUD_CONFIG = { +@@ -53,7 +195,7 @@ BUILTIN_CLOUD_CONFIG = { + 'layout': [100], + 'overwrite': True}, + }, +- 'fs_setup': [{'filesystem': 'ext4', ++ 'fs_setup': [{'filesystem': DEFAULT_FS, + 'device': 'ephemeral0.1', + 'replace_fs': 'ntfs'}], + } +@@ -178,7 +320,11 @@ class DataSourceAzureNet(sources.DataSource): + for cdev in candidates: + try: + if cdev.startswith("/dev/"): +- ret = util.mount_cb(cdev, load_azure_ds_dir) ++ if util.is_FreeBSD(): ++ ret = util.mount_cb(cdev, load_azure_ds_dir, ++ mtype="udf", sync=False) ++ else: ++ ret = util.mount_cb(cdev, load_azure_ds_dir) + else: + ret = load_azure_ds_dir(cdev) + +@@ -206,11 +352,13 @@ class DataSourceAzureNet(sources.DataSource): + LOG.debug("using files cached in %s", ddir) + + # azure / hyper-v provides random data here +- seed = util.load_file("/sys/firmware/acpi/tables/OEM0", +- quiet=True, decode=False) +- if seed: +- self.metadata['random_seed'] = seed + ++ if not util.is_FreeBSD(): ++ seed = util.load_file("/sys/firmware/acpi/tables/OEM0", ++ quiet=True, decode=False) ++ if seed: ++ self.metadata['random_seed'] = seed ++ # TODO. find the seed on FreeBSD platform + # now update ds_cfg to reflect contents pass in config + user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {}) + self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg]) +@@ -619,8 +767,19 @@ def encrypt_pass(password, salt_id="$6$"): + def list_possible_azure_ds_devs(): + # return a sorted list of devices that might have a azure datasource + devlist = [] +- for fstype in ("iso9660", "udf"): +- devlist.extend(util.find_devs_with("TYPE=%s" % fstype)) ++ if util.is_FreeBSD(): ++ cdrom_dev = "/dev/cd0" ++ try: ++ util.subp(["mount", "-o", "ro", "-t", "udf", cdrom_dev, ++ "/mnt/cdrom/secure"]) ++ except util.ProcessExecutionError: ++ LOG.debug("Fail to mount cd") ++ return devlist ++ util.subp(["umount", "/mnt/cdrom/secure"]) ++ devlist.append(cdrom_dev) ++ else: ++ for fstype in ("iso9660", "udf"): ++ devlist.extend(util.find_devs_with("TYPE=%s" % fstype)) + + devlist.sort(reverse=True) + return devlist +--- cloudinit/sources/helpers/azure.py.orig ++++ cloudinit/sources/helpers/azure.py +@@ -29,6 +29,14 @@ def cd(newdir): + os.chdir(prevdir) + + ++def get_azure_endpoint(): ++ if util.is_FreeBSD(): ++ azure_endpoint = "option-245" ++ else: ++ azure_endpoint = "unknown-245" ++ return azure_endpoint ++ ++ + class AzureEndpointHttpClient(object): + + headers = { +@@ -236,7 +244,8 @@ class WALinuxAgentShim(object): + content = util.load_file(fallback_lease_file) + LOG.debug("content is %s", content) + for line in content.splitlines(): +- if 'unknown-245' in line: ++ azure_endpoint = get_azure_endpoint() ++ if azure_endpoint in line: + # Example line from Ubuntu + # option unknown-245 a8:3f:81:10; + leases.append(line.strip(' ').split(' ', 2)[-1].strip(';\n"')) +--- cloudinit/stages.py.orig ++++ cloudinit/stages.py +@@ -616,7 +616,7 @@ class Init(object): + return (None, loc) + if ncfg: + return (ncfg, loc) +- return (net.generate_fallback_config(), "fallback") ++ return (self.distro.generate_fallback_config(), "fallback") + + def apply_network_config(self, bring_up): + netcfg, src = self._find_networking_config() +--- cloudinit/util.py.orig ++++ cloudinit/util.py +@@ -565,6 +565,10 @@ def is_ipv4(instr): + return len(toks) == 4 + + ++def is_FreeBSD(): ++ return system_info()['platform'].startswith('FreeBSD') ++ ++ + def get_cfg_option_bool(yobj, key, default=False): + if key not in yobj: + return default +@@ -2091,11 +2095,56 @@ def parse_mtab(path): + return None + + ++def find_freebsd_part(label_part): ++ if label_part.startswith("/dev/label/"): ++ target_label = label_part[5:] ++ (label_part, err) = subp(['glabel', 'status', '-s']) ++ for labels in label_part.split("\n"): ++ items = labels.split() ++ if len(items) > 0 and items[0].startswith(target_label): ++ label_part = items[2] ++ break ++ label_part = str(label_part) ++ return label_part ++ ++ ++def get_path_dev_freebsd(path, mnt_list): ++ path_found = None ++ for line in mnt_list.split("\n"): ++ items = line.split() ++ if (len(items) > 2 and os.path.exists(items[1] + path)): ++ path_found = line ++ break ++ return path_found ++ ++ ++def get_mount_info_freebsd(path, log=LOG): ++ (result, err) = subp(['mount', '-p', path], rcs=[0, 1]) ++ if len(err): ++ # find a path if the input is not a mounting point ++ (mnt_list, err) = subp(['mount', '-p']) ++ path_found = get_path_dev_freebsd(path, mnt_list) ++ if (path_found is None): ++ return None ++ result = path_found ++ ret = result.split() ++ label_part = find_freebsd_part(ret[0]) ++ return "/dev/" + label_part, ret[2], ret[1] ++ ++ + def parse_mount(path): + (mountoutput, _err) = subp("mount") + mount_locs = mountoutput.splitlines() + for line in mount_locs: + m = re.search(r'^(/dev/[\S]+) on (/.*) \((.+), .+, (.+)\)$', line) ++ if not m: ++ continue ++ # check whether the dev refers to a label on FreeBSD ++ # for example, if dev is '/dev/label/rootfs', we should ++ # continue finding the real device like '/dev/da0'. ++ devm = re.search('^(/dev/.+)p([0-9])$', m.group(1)) ++ if (not devm and is_FreeBSD()): ++ return get_mount_info_freebsd(path) + devpth = m.group(1) + mount_point = m.group(2) + fs_type = m.group(3) +@@ -2357,7 +2406,8 @@ def read_dmi_data(key): + uname_arch = os.uname()[4] + if not (uname_arch == "x86_64" or + (uname_arch.startswith("i") and uname_arch[2:] == "86") or +- uname_arch == 'aarch64'): ++ uname_arch == 'aarch64' or ++ uname_arch == 'amd64'): + LOG.debug("dmidata is not supported on %s", uname_arch) + return None + +--- config/cloud.cfg-freebsd.orig ++++ config/cloud.cfg-freebsd +@@ -5,7 +5,7 @@ syslog_fix_perms: root:wheel + + # This should not be required, but leave it in place until the real cause of + # not beeing able to find -any- datasources is resolved. +-datasource_list: ['ConfigDrive', 'OpenStack', 'Ec2'] ++datasource_list: ['ConfigDrive', 'Azure', 'OpenStack', 'Ec2'] + + # A set of users which may be applied and/or used by various modules + # when a 'default' entry is found it will reference the 'default_user' +--- requirements.txt.orig ++++ requirements.txt +@@ -28,7 +28,7 @@ configobj>=5.0.2 + pyyaml + + # The new main entrypoint uses argparse instead of optparse +-argparse ++# argparse + + # Requests handles ssl correctly! + requests +--- setup.py.orig ++++ setup.py +@@ -89,7 +89,6 @@ LIB = "/lib" + if os.uname()[0] == 'FreeBSD': + USR = "/usr/local" + USR_LIB_EXEC = "/usr/local/lib" +- ETC = "/usr/local/etc" + elif os.path.isfile('/etc/redhat-release'): + USR_LIB_EXEC = "/usr/libexec" + +@@ -166,8 +165,6 @@ else: + (ETC + '/cloud', glob('config/*.cfg')), + (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')), + (ETC + '/cloud/templates', glob('templates/*')), +- (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']), +- (ETC + '/dhcp/dhclient-exit-hooks.d/', ['tools/hook-dhclient']), + (USR_LIB_EXEC + '/cloud-init', ['tools/uncloud-init', + 'tools/write-ssh-key-fingerprints']), + (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]), +@@ -175,8 +172,13 @@ else: + [f for f in glob('doc/examples/*') if is_f(f)]), + (USR + '/share/doc/cloud-init/examples/seed', + [f for f in glob('doc/examples/seed/*') if is_f(f)]), +- (LIB + '/udev/rules.d', [f for f in glob('udev/*.rules')]), + ] ++ if os.uname()[0] != 'FreeBSD': ++ data_files.append([ ++ (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']), ++ (ETC + '/dhcp/dhclient-exit-hooks.d/', ['tools/hook-dhclient']), ++ (LIB + '/udev/rules.d', [f for f in glob('udev/*.rules')]), ++ ]) + # Use a subclass for install that handles + # adding on the right init system configuration files + cmdclass = { +@@ -187,6 +189,9 @@ else: + requirements = read_requires() + if sys.version_info < (3,): + requirements.append('cheetah') ++if ((sys.version_info.major == 2 and sys.version_info.minor < 7) or ++ (sys.version_info.major == 3 and sys.version_info.minor < 2)): ++ requirements.append('argparse') + + setuptools.setup( + name='cloud-init', +--- sysvinit/freebsd/cloudconfig.orig ++++ sysvinit/freebsd/cloudconfig +@@ -7,24 +7,14 @@ + . /etc/rc.subr + + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +-export CLOUD_CFG=/usr/local/etc/cloud/cloud.cfg + + name="cloudconfig" + command="/usr/local/bin/cloud-init" + start_cmd="cloudconfig_start" + stop_cmd=":" + rcvar="cloudinit_enable" +-start_precmd="cloudinit_override" + start_cmd="cloudconfig_start" + +-cloudinit_override() +-{ +- # If there exist sysconfig/defaults variable override files use it... +- if [ -f /etc/defaults/cloud-init ]; then +- . /etc/defaults/cloud-init +- fi +-} +- + cloudconfig_start() + { + echo "${command} starting" +--- sysvinit/freebsd/cloudfinal.orig ++++ sysvinit/freebsd/cloudfinal +@@ -7,24 +7,14 @@ + . /etc/rc.subr + + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +-export CLOUD_CFG=/usr/local/etc/cloud/cloud.cfg + + name="cloudfinal" + command="/usr/local/bin/cloud-init" + start_cmd="cloudfinal_start" + stop_cmd=":" + rcvar="cloudinit_enable" +-start_precmd="cloudinit_override" + start_cmd="cloudfinal_start" + +-cloudinit_override() +-{ +- # If there exist sysconfig/defaults variable override files use it... +- if [ -f /etc/defaults/cloud-init ]; then +- . /etc/defaults/cloud-init +- fi +-} +- + cloudfinal_start() + { + echo -n "${command} starting" +--- sysvinit/freebsd/cloudinit.orig ++++ sysvinit/freebsd/cloudinit +@@ -7,24 +7,14 @@ + . /etc/rc.subr + + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +-export CLOUD_CFG=/usr/local/etc/cloud/cloud.cfg + + name="cloudinit" + command="/usr/local/bin/cloud-init" + start_cmd="cloudinit_start" + stop_cmd=":" + rcvar="cloudinit_enable" +-start_precmd="cloudinit_override" + start_cmd="cloudinit_start" + +-cloudinit_override() +-{ +- # If there exist sysconfig/defaults variable override files use it... +- if [ -f /etc/defaults/cloud-init ]; then +- . /etc/defaults/cloud-init +- fi +-} +- + cloudinit_start() + { + echo -n "${command} starting" +--- sysvinit/freebsd/cloudinitlocal.orig ++++ sysvinit/freebsd/cloudinitlocal +@@ -1,30 +1,20 @@ + #!/bin/sh + + # PROVIDE: cloudinitlocal +-# REQUIRE: mountcritlocal ++# REQUIRE: ldconfig mountcritlocal + # BEFORE: NETWORKING FILESYSTEMS cloudinit cloudconfig cloudfinal + + . /etc/rc.subr + + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +-export CLOUD_CFG=/usr/local/etc/cloud/cloud.cfg + + name="cloudinitlocal" + command="/usr/local/bin/cloud-init" + start_cmd="cloudlocal_start" + stop_cmd=":" + rcvar="cloudinit_enable" +-start_precmd="cloudinit_override" + start_cmd="cloudlocal_start" + +-cloudinit_override() +-{ +- # If there exist sysconfig/defaults variable override files use it... +- if [ -f /etc/defaults/cloud-init ]; then +- . /etc/defaults/cloud-init +- fi +-} +- + cloudlocal_start() + { + echo -n "${command} starting" +--- tests/unittests/test_datasource/test_azure.py.orig ++++ tests/unittests/test_datasource/test_azure.py +@@ -3,6 +3,8 @@ + from cloudinit import helpers + from cloudinit.util import b64e, decode_binary, load_file + from cloudinit.sources import DataSourceAzure ++from cloudinit.util import find_freebsd_part ++from cloudinit.util import get_path_dev_freebsd + + from ..helpers import TestCase, populate_dir, mock, ExitStack, PY26, SkipTest + +@@ -95,6 +97,41 @@ class TestAzureDataSource(TestCase): + for module, name, new in patches: + self.patches.enter_context(mock.patch.object(module, name, new)) + ++ def _get_mockds(self): ++ mod = DataSourceAzure ++ sysctl_out = "dev.storvsc.3.%pnpinfo: "\ ++ "classid=ba6163d9-04a1-4d29-b605-72e2ffb1dc7f "\ ++ "deviceid=f8b3781b-1e82-4818-a1c3-63d806ec15bb\n" ++ sysctl_out += "dev.storvsc.2.%pnpinfo: "\ ++ "classid=ba6163d9-04a1-4d29-b605-72e2ffb1dc7f "\ ++ "deviceid=f8b3781a-1e82-4818-a1c3-63d806ec15bb\n" ++ sysctl_out += "dev.storvsc.1.%pnpinfo: "\ ++ "classid=32412632-86cb-44a2-9b5c-50d1417354f5 "\ ++ "deviceid=00000000-0001-8899-0000-000000000000\n" ++ camctl_devbus = """ ++scbus0 on ata0 bus 0 ++scbus1 on ata1 bus 0 ++scbus2 on blkvsc0 bus 0 ++scbus3 on blkvsc1 bus 0 ++scbus4 on storvsc2 bus 0 ++scbus5 on storvsc3 bus 0 ++scbus-1 on xpt0 bus 0 ++ """ ++ camctl_dev = """ ++ at scbus1 target 0 lun 0 (cd0,pass0) ++ at scbus2 target 0 lun 0 (da0,pass1) ++ at scbus3 target 1 lun 0 (da1,pass2) ++ """ ++ self.apply_patches([ ++ (mod, 'get_dev_storvsc_sysctl', mock.MagicMock( ++ return_value=sysctl_out)), ++ (mod, 'get_camcontrol_dev_bus', mock.MagicMock( ++ return_value=camctl_devbus)), ++ (mod, 'get_camcontrol_dev', mock.MagicMock( ++ return_value=camctl_dev)) ++ ]) ++ return mod ++ + def _get_ds(self, data, agent_command=None): + + def dsdevs(): +@@ -177,6 +214,34 @@ class TestAzureDataSource(TestCase): + return + raise AssertionError("XML is the same") + ++ def test_get_resource_disk(self): ++ ds = self._get_mockds() ++ dev = ds.get_resource_disk_on_freebsd(1) ++ self.assertEqual("da1", dev) ++ ++ @mock.patch('cloudinit.util.subp') ++ def test_find_freebsd_part_on_Azure(self, mock_subp): ++ glabel_out = ''' ++gptid/fa52d426-c337-11e6-8911-00155d4c5e47 N/A da0p1 ++ label/rootfs N/A da0p2 ++ label/swap N/A da0p3 ++''' ++ mock_subp.return_value = (glabel_out, "") ++ res = find_freebsd_part("/dev/label/rootfs") ++ self.assertEqual("da0p2", res) ++ ++ def test_get_path_dev_freebsd_on_Azure(self): ++ mnt_list = ''' ++/dev/label/rootfs / ufs rw 1 1 ++devfs /dev devfs rw,multilabel 0 0 ++fdescfs /dev/fd fdescfs rw 0 0 ++/dev/da1s1 /mnt/resource ufs rw 2 2 ++''' ++ with mock.patch.object(os.path, 'exists', ++ return_value=True): ++ res = get_path_dev_freebsd('/etc', mnt_list) ++ self.assertNotEqual(res, None) ++ + def test_basic_seed_dir(self): + odata = {'HostName': "myhost", 'UserName': "myuser"} + data = {'ovfcontent': construct_valid_ovf_env(data=odata), +--- tests/unittests/test_datasource/test_azure_helper.py.orig ++++ tests/unittests/test_datasource/test_azure_helper.py +@@ -72,10 +72,11 @@ class TestFindEndpoint(TestCase): + + @staticmethod + def _build_lease_content(encoded_address): ++ endpoint = azure_helper.get_azure_endpoint() + return '\n'.join([ + 'lease {', + ' interface "eth0";', +- ' option unknown-245 {0};'.format(encoded_address), ++ ' option {0} {1};'.format(endpoint, encoded_address), + '}']) + + def test_from_dhcp_client(self): +--- tests/unittests/test_datasource/test_cloudstack.py.orig ++++ tests/unittests/test_datasource/test_cloudstack.py +@@ -15,6 +15,11 @@ class TestCloudStackPasswordFetching(TestCase): + mod_name = 'cloudinit.sources.DataSourceCloudStack' + self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name))) + self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name))) ++ default_gw = "192.201.20.0" ++ mod_name = 'cloudinit.sources.DataSourceCloudStack.get_default_gateway' ++ get_default_gw = mock.MagicMock(return_value=default_gw) ++ self.patches.enter_context( ++ mock.patch(mod_name, get_default_gw)) + + def _set_password_server_response(self, response_string): + subp = mock.MagicMock(return_value=(response_string, '')) +--- tests/unittests/test_distros/test_netconfig.py.orig ++++ tests/unittests/test_distros/test_netconfig.py +@@ -83,6 +83,20 @@ class WriteBuffer(object): + + class TestNetCfgDistro(TestCase): + ++ frbsd_ifout = """\ ++hn0: flags=8843 metric 0 mtu 1500 ++ options=51b ++ ether 00:15:5d:4c:73:00 ++ inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2 ++ inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255 ++ nd6 options=23 ++ media: Ethernet autoselect (10Gbase-T ) ++ status: active ++""" ++ ++ def setUp(self): ++ super(TestNetCfgDistro, self).setUp() ++ + def _get_distro(self, dname): + cls = distros.fetch(dname) + cfg = settings.CFG_BUILTIN +@@ -127,6 +141,29 @@ class TestNetCfgDistro(TestCase): + for (k, v) in b1.items(): + self.assertEqual(v, b2[k]) + ++ @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list') ++ @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out') ++ def test_get_ip_nic_freebsd(self, ifname_out, iflist): ++ frbsd_distro = self._get_distro('freebsd') ++ iflist.return_value = "lo0 hn0" ++ ifname_out.return_value = self.frbsd_ifout ++ res = frbsd_distro.get_ipv4() ++ self.assertEqual(res, ['lo0', 'hn0']) ++ res = frbsd_distro.get_ipv6() ++ self.assertEqual(res, []) ++ ++ @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether') ++ @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out') ++ @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac') ++ def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether): ++ frbsd_distro = self._get_distro('freebsd') ++ ++ if_ether.return_value = 'hn0' ++ ifname_out.return_value = self.frbsd_ifout ++ mac.return_value = '00:15:5d:4c:73:00' ++ res = frbsd_distro.generate_fallback_config() ++ self.assertIsNotNone(res) ++ + def test_simple_write_rh(self): + rh_distro = self._get_distro('rhel') + +--- tests/unittests/test_util.py.orig ++++ tests/unittests/test_util.py +@@ -567,7 +567,8 @@ class TestSubp(helpers.TestCase): + def test_subp_capture_stderr(self): + data = b'hello world' + (out, err) = util.subp(self.stdin2err, capture=True, +- decode=False, data=data) ++ decode=False, data=data, ++ update_env={'LC_ALL': 'C'}) + self.assertEqual(err, data) + self.assertEqual(out, b'') + +--- tools/build-on-freebsd.orig ++++ tools/build-on-freebsd +@@ -3,16 +3,14 @@ + # installing cloud-init. This script takes care of building and installing. It + # will optionally make a first run at the end. + +-fail() { echo "FAILED:" "$@" 1>&2; exit 1; } ++fail() { echo "FAILED:" "$@" 1>&2; exit 1;} + + # Check dependencies: + depschecked=/tmp/c-i.dependencieschecked + pkgs=" + dmidecode + e2fsprogs +- gpart + py27-Jinja2 +- py27-argparse + py27-boto + py27-cheetah + py27-configobj +@@ -38,7 +36,7 @@ python setup.py build + python setup.py install -O1 --skip-build --prefix /usr/local/ --init-system sysvinit_freebsd + + # Install the correct config file: +-cp config/cloud.cfg-freebsd /usr/local/etc/cloud/cloud.cfg ++cp config/cloud.cfg-freebsd /etc/cloud/cloud.cfg + + # Enable cloud-init in /etc/rc.conf: + sed -i.bak -e "/cloudinit_enable=.*/d" /etc/rc.conf Index: net/cloud-init-azure/pkg-descr =================================================================== --- /dev/null +++ net/cloud-init-azure/pkg-descr @@ -0,0 +1,3 @@ +Package provides configuration and customization of Azure instance. + +WWW: https://launchpad.net/cloud-init Index: net/cloud-init-azure/pkg-message =================================================================== --- /dev/null +++ net/cloud-init-azure/pkg-message @@ -0,0 +1,7 @@ +========================================================== +To enable cloud-init, add the following line to rc.conf: + +cloudinit_enable="YES" + +This will make sure cloud-init is started at boot. +==========================================================