Repository: Azure/WALinuxAgent
Pull request #2553 (2022-04-14 12:20:58)

Added support for devuan linux distribution
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Issue # <!-- if any -->
<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

Added support for linux distribution devuan 

Will only work on devuan version 4 (chimaera) or later. In earlier versions of devuan, waagent uses the python module platform.linux_distribution to get the distribution details. If run under devuan, this incorrectly reports "debian" as the distro.

Devuan version 4 uses python 3.9, and in this, platform.linux_distribution has been removed, forcing waagent to use
distro.linux_distribution instead: this correctly identifies devuan.

---

### PR information
- [y ] The title of the PR is clear and informative.
- [y] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [x] If applicable, the PR references the bug/issue that it fixes in the description.
- [y ] New Unit tests were added for the changes made

### Quality of Code and Contribution Guidelines
- [y ] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) Report
> Merging [#2553](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (d4ac6f3) into [develop](https://codecov.io/gh/Azure/WALinuxAgent/commit/1da0ef6e3c0be655a890f621e38b97b0820f4971?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (1da0ef6) will **increase** coverage by `0.07%`.
> The diff coverage is `62.96%`.

```diff
@@             Coverage Diff             @@
##           develop    #2553      +/-   ##
===========================================
+ Coverage    71.84%   71.92%   +0.07%     
===========================================
  Files          102      103       +1     
  Lines        15321    15424     +103     
  Branches      2429     2448      +19     
===========================================
+ Hits         11008    11094      +86     
- Misses        3822     3836      +14     
- Partials       491      494       +3     
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/osutil/devuan.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZGV2dWFuLnB5) | `58.33% <58.33%> (ø)` | |
| [azurelinuxagent/common/osutil/factory.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZmFjdG9yeS5weQ==) | `95.29% <100.00%> (+0.17%)` | :arrow_up: |
| [azurelinuxagent/common/utils/timeutil.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi91dGlscy90aW1ldXRpbC5weQ==) | `61.53% <0.00%> (-27.36%)` | :arrow_down: |
| [azurelinuxagent/common/utils/archive.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi91dGlscy9hcmNoaXZlLnB5) | `88.65% <0.00%> (-1.21%)` | :arrow_down: |
| [azurelinuxagent/common/logger.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9sb2dnZXIucHk=) | `89.83% <0.00%> (-0.98%)` | :arrow_down: |
| [azurelinuxagent/common/protocol/goal\_state.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9nb2FsX3N0YXRlLnB5) | `95.25% <0.00%> (-0.01%)` | :arrow_down: |
| [azurelinuxagent/ga/exthandlers.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL2V4dGhhbmRsZXJzLnB5) | `85.87% <0.00%> (+0.08%)` | :arrow_up: |
| [azurelinuxagent/ga/update.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL3VwZGF0ZS5weQ==) | `89.35% <0.00%> (+0.12%)` | :arrow_up: |
| [azurelinuxagent/common/cgroupconfigurator.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBjb25maWd1cmF0b3IucHk=) | `74.24% <0.00%> (+0.35%)` | :arrow_up: |
| [azurelinuxagent/common/utils/fileutil.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi91dGlscy9maWxldXRpbC5weQ==) | `79.69% <0.00%> (+0.75%)` | :arrow_up: |
| ... and [1 more](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Last update [1da0ef6...d4ac6f3](https://codecov.io/gh/Azure/WALinuxAgent/pull/2553?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure).

narrieta: @peter9370 Automation looks OK. Would you check my comments wrt commented-out code and update as needed? I can merge after that. Thanks!

Patch:
diff --git a/azurelinuxagent/common/osutil/devuan.py b/azurelinuxagent/common/osutil/devuan.py
new file mode 100644
index 00000000..a482cd05
--- /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 61c2e6d2..e799ddcd 100644
--- a/azurelinuxagent/common/osutil/factory.py
+++ b/azurelinuxagent/common/osutil/factory.py
@@ -27,6 +27,7 @@ from .clearlinux import ClearLinuxUtil
 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 00000000..be80edbd
--- /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 00000000..02532025
--- /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 00000000..0d967c0c
--- /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 f9294358..f90ea70e 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"])


Test patch:
diff --git a/tests/common/osutil/test_factory.py b/tests/common/osutil/test_factory.py
index 50072427..7bd729c3 100644
--- a/tests/common/osutil/test_factory.py
+++ b/tests/common/osutil/test_factory.py
@@ -21,6 +21,7 @@ from azurelinuxagent.common.osutil.bigip import BigIpOSUtil
 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 @@ class TestOsUtilFactory(AgentTestCase):
         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="",