diff --git a/azurelinuxagent/common/osutil/devuan.py b/azurelinuxagent/common/osutil/devuan.py new file mode 100644 index 0000000000..a482cd05a0 --- /dev/null +++ b/azurelinuxagent/common/osutil/devuan.py @@ -0,0 +1,52 @@ +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.shellutil as shellutil +from azurelinuxagent.common.osutil.default import DefaultOSUtil + + +class DevuanOSUtil(DefaultOSUtil): + + def __init__(self): + super(DevuanOSUtil, self).__init__() + self.jit_enabled = True + + def restart_ssh_service(self): + logger.info("DevuanOSUtil::restart_ssh_service - trying to restart sshd") + return shellutil.run("/usr/sbin/service restart ssh", chk_err=False) + + def stop_agent_service(self): + logger.info("DevuanOSUtil::stop_agent_service - trying to stop waagent") + return shellutil.run("/usr/sbin/service walinuxagent stop", chk_err=False) + + def start_agent_service(self): + logger.info("DevuanOSUtil::start_agent_service - trying to start waagent") + return shellutil.run("/usr/sbin/service walinuxagent start", chk_err=False) + + def start_network(self): + pass + + def remove_rules_files(self, rules_files=""): + pass + + def restore_rules_files(self, rules_files=""): + pass + + def get_dhcp_lease_endpoint(self): + return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py index 61c2e6d20d..e799ddcd99 100644 --- a/azurelinuxagent/common/osutil/factory.py +++ b/azurelinuxagent/common/osutil/factory.py @@ -27,6 +27,7 @@ from .coreos import CoreOSUtil from .debian import DebianOSBaseUtil, DebianOSModernUtil from .default import DefaultOSUtil +from .devuan import DevuanOSUtil from .freebsd import FreeBSDOSUtil from .gaia import GaiaOSUtil from .iosxe import IosxeOSUtil @@ -102,6 +103,16 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) return DebianOSBaseUtil() + # Devuan support only works with v4+ + # Reason is that Devuan v4 (Chimaera) uses python v3.9, in which the + # platform.linux_distribution module has been removed. This was unable + # to distinguish between debian and devuan. The new distro.linux_distribution module + # is able to distinguish between the two. + + if distro_name == "devuan" and Version(distro_version) >= Version("4"): + return DevuanOSUtil() + + if distro_name in ("redhat", "rhel", "centos", "oracle", "almalinux", "cloudlinux", "rocky"): if Version(distro_version) < Version("7"): diff --git a/config/devuan/waagent.conf b/config/devuan/waagent.conf new file mode 100644 index 0000000000..be80edbd42 --- /dev/null +++ b/config/devuan/waagent.conf @@ -0,0 +1,130 @@ +# +# Microsoft Azure Linux Agent Configuration +# + +# Enable extension handling. Do not disable this unless you do not need password reset, +# backup, monitoring, or any extension handling whatsoever. +Extensions.Enabled=y + +# Which provisioning agent to use. Supported values are "auto" (default), "waagent", +# "cloud-init", or "disabled". +Provisioning.Agent=auto + +# Password authentication for root account will be unavailable. +Provisioning.DeleteRootPassword=y + +# Generate fresh host key pair. +Provisioning.RegenerateSshHostKeyPair=y + +# Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". +# The "auto" option is supported on OpenSSH 5.9 (2011) and later. +Provisioning.SshHostKeyPairType=auto + +# Monitor host name changes and publish changes via DHCP requests. +Provisioning.MonitorHostName=y + +# Decode CustomData from Base64. +Provisioning.DecodeCustomData=n + +# Execute CustomData after provisioning. +Provisioning.ExecuteCustomData=n + +# Algorithm used by crypt when generating password hash. +#Provisioning.PasswordCryptId=6 + +# Length of random salt used when generating password hash. +#Provisioning.PasswordCryptSaltLength=10 + +# Allow reset password of sys user +Provisioning.AllowResetSysUser=n + +# Format if unformatted. If 'n', resource disk will not be mounted. +ResourceDisk.Format=y + +# File system on the resource disk +# Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. +ResourceDisk.Filesystem=ext4 + +# Mount point for the resource disk +ResourceDisk.MountPoint=/mnt/resource + +# Create and use swapfile on resource disk. +ResourceDisk.EnableSwap=n + +# Size of the swapfile. +ResourceDisk.SwapSizeMB=0 + +# Comma-separated list of mount options. See mount(8) for valid options. +ResourceDisk.MountOptions=None + +# Enable verbose logging (y|n) +Logs.Verbose=n + +# Enable Console logging, default is y +# Logs.Console=y + +# Is FIPS enabled +OS.EnableFIPS=n + +# Root device timeout in seconds. +OS.RootDeviceScsiTimeout=300 + +# If "None", the system default version is used. +OS.OpensslPath=None + +# Set the SSH ClientAliveInterval +# OS.SshClientAliveInterval=180 + +# Set the path to SSH keys and configuration files +OS.SshDir=/etc/ssh + +# If set, agent will use proxy server to access internet +#HttpProxy.Host=None +#HttpProxy.Port=None + +# Detect Scvmm environment, default is n +# DetectScvmmEnv=n + +# +# Lib.Dir=/var/lib/waagent + +# +# DVD.MountPoint=/mnt/cdrom/secure + +# +# Pid.File=/var/run/waagent.pid + +# +# Extension.LogDir=/var/log/azure + +# +# Home.Dir=/home + +# Enable RDMA management and set up, should only be used in HPC images +# OS.EnableRDMA=y + +# Enable or disable goal state processing auto-update, default is enabled +# AutoUpdate.Enabled=y + +# Determine the update family, this should not be changed +# AutoUpdate.GAFamily=Prod + +# Determine if the overprovisioning feature is enabled. If yes, hold extension +# handling until inVMArtifactsProfile.OnHold is false. +# Default is enabled +# EnableOverProvisioning=y + +# Allow fallback to HTTP if HTTPS is unavailable +# Note: Allowing HTTP (vs. HTTPS) may cause security risks +# OS.AllowHTTP=n + +# Add firewall rules to protect access to Azure host node services +# Note: +# - The default is false to protect the state of existing VMs +OS.EnableFirewall=y + +# Enforce control groups limits on the agent and extensions +CGroups.EnforceLimits=n + +# CGroups which are excluded from limits, comma separated +CGroups.Excluded=customscript,runcommand diff --git a/init/devuan/default/walinuxagent b/init/devuan/default/walinuxagent new file mode 100644 index 0000000000..025320250e --- /dev/null +++ b/init/devuan/default/walinuxagent @@ -0,0 +1,2 @@ +# To disable the Microsoft Azure Agent, set WALINUXAGENT_ENABLED=0 +WALINUXAGENT_ENABLED=1 diff --git a/init/devuan/walinuxagent b/init/devuan/walinuxagent new file mode 100644 index 0000000000..0d967c0cc8 --- /dev/null +++ b/init/devuan/walinuxagent @@ -0,0 +1,344 @@ +#!/bin/bash +# walinuxagent +# script to start and stop the waagent daemon. +# +# This script takes into account the possibility that both daemon and +# non-daemon instances of waagent may be running concurrently, +# and attempts to ensure that any non-daemon instances are preserved +# when the daemon instance is stopped. +# +### BEGIN INIT INFO +# Provides: walinuxagent +# Required-Start: $remote_fs $syslog $network +# Required-Stop: $remote_fs +# X-Start-Before: cloud-init +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Microsoft Azure Linux Agent +### END INIT INFO + +DESC="Microsoft Azure Linux Agent" +INTERPRETER="/usr/bin/python3" +DAEMON='/usr/sbin/waagent' +DAEMON_ARGS='-daemon' +START_ARGS='--background' +NAME='waagent' +# set to 1 to enable a lot of debugging output +DEBUG=0 + +. /lib/lsb/init-functions + +debugmsg() { + # output a console message if DEBUG is set + # (can be enabled dynamically by giving "debug" as an extra argument) + if [ "x${DEBUG}" == "x1" ] ; then + echo "[debug]: $1" >&2 + fi + return 0 +} + +check_non_daemon_instances() { + # check if there are any non-daemon instances of waagent running + local NDPIDLIST i NDPIDCT + declare -a NDPIDLIST + debugmsg "check_non_daemon_instance: after init, #NDPIDLIST=${#NDPIDLIST[*]}" + readarray -t NDPIDLIST < <( ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -v -- "${DAEMON_ARGS}" | + grep -v "grep" | + awk '{ print $1 }') + NDPIDCT=${#NDPIDLIST[@]} + debugmsg "check_non_daemon_instances: NDPIDCT=${NDPIDCT}" + debugmsg "check_non_daemon_instances: NDPIDLIST[0] = ${NDPIDLIST[0]}" + if [ ${NDPIDCT} -gt 0 ] ; then + debugmsg "check_non_daemon_instances: WARNING: non-daemon instances of waagent exist" + else + debugmsg "check_non_daemon_instances: no non-daemon instances of waagent are currently running" + fi + for (( i = 0 ; i < ${NDPIDCT} ; i++ )) ; do + debugmsg "check_non_daemon_instances: WARNING: process ${NDPIDLIST[${i}]} is a non-daemon waagent instance" + done + return 0 +} + +get_daemon_pid() { + # (re)create PIDLIST, return the first entry + local PID + create_pidlist + PID=${PIDLIST[0]} + if [ -z "${PID}" ] ; then + debugmsg "get_daemon_pid: : WARNING: no waagent daemon process found" + fi + echo "${PID}" +} + +recheck_status() { + # after an attempt to stop the daemon, re-check the status + # and take any further actions required. + # (NB: at the moment, we only re-check once. Possible improvement + # would be to iterate the re-check up to a given maximum tries). + local STATUS NEWSTATUS + get_status + STATUS=$? + debugmsg "stop_waagent: status is now ${STATUS}" + # ideal if stop has been successful: STATUS=1 - no daemon process + case ${STATUS} in + 0) + # stop didn't work + # what to do? maybe try kill -9 ? + debugmsg "recheck_status: ERROR: unable to stop waagent" + debugmsg "recheck_status: trying again with kill -9" + kill_daemon_from_pid 1 + # probably need to check status again? + get_status + NEW_STATUS=$? + if [ "x${NEW_STATUS}" == "x1" ] ; then + debugmsg "recheck_status: successfully stopped." + log_end_msg 0 || true + else + # could probably do something more productive here + debugmsg "recheck_status: unable to stop daemon - giving up" + log_end_msg 1 || true + exit 1 + fi + ;; + 1) + # THIS IS THE EXPECTED CASE: daemon is no longer running and + debugmsg "recheck_status: waagent daemon stopped successfully." + log_end_msg 0 || true + ;; + 2) + # so weird that we can't figure out what's going on + debugmsg "recheck_status: ERROR: unable to determine waagent status" + debugmsg "recheck_status: manual intervention required" + log_end_msg 1 || true + exit 1 + ;; + esac +} + +start_waagent() { + # we use start-stop-daemon for starting waagent + local STATUS + get_status + STATUS=$? + # check the status value - take appropriate action + debugmsg "start_waagent: STATUS=${STATUS}" + case "${STATUS}" in + 0) + debugmsg "start_waagent: waagent is already running" + log_daemon_msg "waagent is already running" + log_end_msg 0 || true + ;; + 1) + # not running (we ignore presence/absence of pidfile) + # just start waagent + debugmsg "start_waagent: waagent is not currently running" + log_daemon_msg "Starting ${NAME} daemon" + start-stop-daemon --start --quiet --background --name "${NAME}" --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} + log_end_msg $? || true + ;; + 2) + # get_status can't figure out what's going on. + # try doing a stop to clean up, then attempt to start waagent + # will probably require manual intervention + debugmsg "start_waagent: unable to determine current status" + debugmsg "start_waagent: trying to stop waagent first, and then start it" + stop_waagent + log_daemon_msg "Starting ${NAME} daemon" + start-stop-daemon --start --quiet --background --name ${NAME} --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} + log_end_msg $? || true + ;; + esac +} + +kill_daemon_from_pidlist() { + # check the pidlist for at least one waagent daemon process + # if found, kill it directly from the entry in the pidlist + # Ignore any pidfile. Avoid killing any non-daemon + # waagent processes. + # If called with "1" as first argument, use kill -9 rather than + # normal kill + local i PIDCT FORCE + FORCE=0 + if [ "x${1}" == "x1" ] ; then + debugmsg "kill_daemon_from_pidlist: WARNING: using kill -9" + FORCE=1 + fi + debugmsg "kill_daemon_from_pidlist: killing daemon using pid(s) in PIDLIST" + PIDCT=${#PIDLIST[*]} + if [ "${PIDCT}" -eq 0 ] ; then + debugmsg "kill_daemon_from_pidlist: ERROR: no pids in PIDLIST" + return 1 + fi + for (( i=0 ; i < ${PIDCT} ; i++ )) ; do + debugmsg "kill_daemon_from_pidlist: killing waagent daemon process ${PIDLIST[${i}]}" + if [ "x${FORCE}" == "x1" ] ; then + kill -9 ${PIDLIST[${i}]} + else + kill ${PIDLIST[${i}]} + fi + done + return 0 +} + +stop_waagent() { + # check the current status and if the waagent daemon is running, attempt + # to stop it. + # start-stop-daemon is avoided here + local STATUS PID RC + get_status + STATUS=$? + debugmsg "stop_waagent: current status = ${STATUS}" + case "${STATUS}" in + 0) + # - ignore any pidfile - kill directly from process list + log_daemon_msg "Stopping ${NAME} daemon (using process list)" + kill_daemon_from_pidlist + recheck_status + ;; + 1) + # not running - we ignore any pidfile + # REVISIT: should we check for a pidfile and remove if found? + debugmsg "waagent is not running" + log_daemon_msg "waagent is already stopped" + log_end_msg 0 || true + ;; + 2) + # weirdness - call for help + debugmsg "ERROR: unable to determine waagent status - manual intervention required" + log_daemon_msg "WARNING: unable to determine status of waagent daemon - manual intervention required" + log_end_msg 1 || true + ;; + esac +} + +check_daemons() { + # check for running waagent daemon processes + local ENTRY + ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -- "${DAEMON_ARGS}" | + grep -v 'grep' | + while read ENTRY ; do + debugmsg "check_daemons(): ENTRY='${ENTRY}'" + done + return 0 +} + +create_pidlist() { + # initialise the list of waagent daemon processes + # NB: there should only be one - both this script and waagent itself + # attempt to avoid starting more than one daemon process. + # However, we use an array just in case. + readarray -t PIDLIST < <( ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -- "${DAEMON_ARGS}" | + grep -v 'grep' | + awk '{ print $1 }') + if [ "${#PIDLIST[*]}" -eq 0 ] ; then + debugmsg "create_pidlist: WARNING: no waagent daemons found" + elif [ "${#PIDLIST[*]}" -gt 1 ] ; then + debugmsg "create_pidlist: WARNING: multiple waagent daemons running" + fi + return 0 +} + +get_status() { + # simplified status - ignoring any pidfile + # Possibilities: + # 0 - waagent daemon running + # 1 - waagent daemon not running + # 2 - status unclear + # (NB: if we find that multiple daemons exist, we just ignore the fact. + # It should be virtually impossible for this to happen) + local FOUND RPID ENTRY STATUS DAEMON_RUNNING PIDCT + PIDCT=0 + DAEMON_RUNNING= + RPID= + ENTRY= + # assume the worst + STATUS=2 + check_daemons + create_pidlist + # should only be one daemon running - but we check, just in case + PIDCT=${#PIDLIST[@]} + debugmsg "get_status: PIDCT=${PIDCT}" + if [ ${PIDCT} -eq 0 ] ; then + # not running + STATUS=1 + else + # at least one daemon process is running + if [ ${PIDCT} -gt 1 ] ; then + debugmsg "get_status: WARNING: more than one waagent daemon running" + debugmsg "get_status: (should not happen)" + else + debugmsg "get_status: only one daemon instance running - as expected" + fi + STATUS=0 + fi + return ${STATUS} +} + +waagent_status() { + # get the current status of the waagent daemon, and return it + local STATUS + get_status + STATUS=$? + debugmsg "waagent status = ${STATUS}" + case ${STATUS} in + 0) + log_daemon_msg "waagent is running" + ;; + 1) + log_daemon_msg "WARNING: waagent is not running" + ;; + 2) + log_daemon_msg "WARNING: waagent status cannot be determined" + ;; + esac + log_end_msg 0 || true + return 0 +} + + +######################################################################### +# MAINLINE +# Usage: "service [scriptname] [ start | stop | status | restart ] [ debug ] +# (specifying debug as extra argument enables debugging output) +######################################################################### + +export PATH="${PATH}:+$PATH:}/usr/sbin:/sbin" + +declare -a PIDLIST + +if [ ! -z "$2" -a "$2" == "debug" ] ; then + DEBUG=1 +fi + +# pre-check for non-daemon (e.g. console) instances of waagent +check_non_daemon_instances + +case "$1" in + start) + start_waagent + ;; + + stop) + stop_waagent + ;; + + status) + waagent_status + ;; + + restart) + stop_waagent + start_waagent + ;; + +esac +exit 0 diff --git a/setup.py b/setup.py index f929435852..f90ea70e4a 100755 --- a/setup.py +++ b/setup.py @@ -219,6 +219,16 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 set_udev_files(data_files, dest="/lib/udev/rules.d") if debian_has_systemd(): set_systemd_files(data_files, dest=systemd_dir_path) + elif name == 'devuan': + set_bin_files(data_files, dest=agent_bin_path, + src=["bin/py3/waagent", "bin/waagent2.0"]) + set_files(data_files, dest="/etc/init.d", + src=['init/devuan/walinuxagent']) + set_files(data_files, dest="/etc/default", + src=['init/devuan/default/walinuxagent']) + set_conf_files(data_files, src=['config/devuan/waagent.conf']) + set_logrotate_files(data_files) + set_udev_files(data_files, dest="/lib/udev/rules.d") elif name == 'iosxe': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/iosxe/waagent.conf"]) diff --git a/tests/common/osutil/test_factory.py b/tests/common/osutil/test_factory.py index 5007242733..7bd729c3b3 100644 --- a/tests/common/osutil/test_factory.py +++ b/tests/common/osutil/test_factory.py @@ -21,6 +21,7 @@ from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil from azurelinuxagent.common.osutil.coreos import CoreOSUtil from azurelinuxagent.common.osutil.debian import DebianOSBaseUtil, DebianOSModernUtil +from azurelinuxagent.common.osutil.devuan import DevuanOSUtil from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.osutil.factory import _get_osutil from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil @@ -187,6 +188,14 @@ def test_get_osutil_it_should_return_debian(self): self.assertTrue(isinstance(ret, DebianOSModernUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") + def test_get_osutil_it_should_return_devuan(self): + ret = _get_osutil(distro_name="devuan", + distro_code_name="", + distro_full_name="", + distro_version="4") + self.assertTrue(isinstance(ret, DevuanOSUtil)) + self.assertEqual(ret.get_service_name(), "waagent") + def test_get_osutil_it_should_return_redhat(self): ret = _get_osutil(distro_name="redhat", distro_code_name="",