import os.path
from collections import OrderedDict
import subprocess
from tempfile import mkdtemp
from shutil import rmtree
from textwrap import dedent

import apt_pkg
import imp

from charmhelpers import osplatform
from mock import patch, call, mock_open
from testtools import TestCase
from tests.helpers import patch_open
from tests.helpers import mock_open as mocked_open
import six

from charmhelpers.core import host


MOUNT_LINES = ("""
rootfs / rootfs rw 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
udev /dev devtmpfs rw,relatime,size=8196788k,nr_inodes=2049197,mode=755 0 0
devpts /dev/pts devpts """
               """rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
""").strip().split('\n')

LSB_RELEASE = '''DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=13.10
DISTRIB_CODENAME=saucy
DISTRIB_DESCRIPTION="Ubuntu Saucy Salamander (development branch)"
'''

OS_RELEASE = '''NAME="CentOS Linux"
ANSI_COLOR="0;31"
ID_LIKE="rhel fedora"
VERSION_ID="7"
BUG_REPORT_URL="https://bugs.centos.org/"
CENTOS_MANTISBT_PROJECT="CentOS-7"
PRETTY_NAME="CentOS Linux 7 (Core)"
VERSION="7 (Core)"
REDHAT_SUPPORT_PRODUCT_VERSION="7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
HOME_URL="https://www.centos.org/"
CPE_NAME="cpe:/o:centos:centos:7"
ID="centos"
'''

IP_LINE_ETH0 = b"""
2: eth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP qlen 1000
    link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff
"""

IP_LINE_ETH100 = b"""
2: eth100: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP qlen 1000
    link/ether e4:11:5b:ab:a7:3d brd ff:ff:ff:ff:ff:ff
"""

IP_LINE_ETH0_VLAN = b"""
6: eth0.10@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 08:00:27:16:b9:5f brd ff:ff:ff:ff:ff:ff
"""

IP_LINE_ETH1 = b"""
3: eth1: <BROADCAST,MULTICAST> mtu 1546 qdisc noop state DOWN qlen 1000
    link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff
"""

IP_LINE_HWADDR = b"""2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000\    link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff"""

IP_LINES = IP_LINE_ETH0 + IP_LINE_ETH1 + IP_LINE_ETH0_VLAN + IP_LINE_ETH100

IP_LINE_BONDS = b"""
6: bond0.10@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 08:00:27:16:b9:5f brd ff:ff:ff:ff:ff:ff
"""


class HelpersTest(TestCase):

    @patch('charmhelpers.core.host.lsb_release')
    @patch('os.path')
    def test_init_is_systemd_upstart(self, path, lsb_release):
        """Upstart based init is correctly detected"""
        lsb_release.return_value = {'DISTRIB_CODENAME': 'whatever'}
        path.isdir.return_value = False
        self.assertFalse(host.init_is_systemd())
        path.isdir.assert_called_with('/run/systemd/system')

    @patch('charmhelpers.core.host.lsb_release')
    @patch('os.path')
    def test_init_is_systemd_system(self, path, lsb_release):
        """Systemd based init is correctly detected"""
        lsb_release.return_value = {'DISTRIB_CODENAME': 'whatever'}
        path.isdir.return_value = True
        self.assertTrue(host.init_is_systemd())
        path.isdir.assert_called_with('/run/systemd/system')

    @patch('charmhelpers.core.host.lsb_release')
    @patch('os.path')
    def test_init_is_systemd_trusty(self, path, lsb_release):
        # Never returns true under trusty, even if the systemd
        # packages have been installed. lp:1670944
        lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'}
        path.isdir.return_value = True
        self.assertFalse(host.init_is_systemd())
        self.assertFalse(path.isdir.called)

    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_runs_service_action(self, mock_call, systemd):
        systemd.return_value = False
        mock_call.return_value = 0
        action = 'some-action'
        service_name = 'foo-service'

        result = host.service(action, service_name)

        self.assertTrue(result)
        mock_call.assert_called_with(['service', service_name, action])

    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_runs_systemctl_action(self, mock_call, systemd):
        """Ensure that service calls under systemd call 'systemctl'."""
        systemd.return_value = True
        mock_call.return_value = 0
        action = 'some-action'
        service_name = 'foo-service'

        result = host.service(action, service_name)

        self.assertTrue(result)
        mock_call.assert_called_with(['systemctl', action, service_name])

    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_returns_false_when_service_fails(self, mock_call, systemd):
        systemd.return_value = False
        mock_call.return_value = 1
        action = 'some-action'
        service_name = 'foo-service'

        result = host.service(action, service_name)

        self.assertFalse(result)
        mock_call.assert_called_with(['service', service_name, action])

    @patch.object(host, 'service')
    def test_starts_a_service(self, service):
        service_name = 'foo-service'
        service.side_effect = [True]
        self.assertTrue(host.service_start(service_name))

        service.assert_called_with('start', service_name)

    @patch.object(host, 'service')
    def test_starts_a_service_with_parms(self, service):
        service_name = 'foo-service'
        service.side_effect = [True]
        self.assertTrue(host.service_start(service_name, id=4))

        service.assert_called_with('start', service_name, id=4)

    @patch.object(host, 'service')
    def test_stops_a_service(self, service):
        service_name = 'foo-service'
        service.side_effect = [True]
        self.assertTrue(host.service_stop(service_name))

        service.assert_called_with('stop', service_name)

    @patch.object(host, 'service')
    def test_restarts_a_service(self, service):
        service_name = 'foo-service'
        service.side_effect = [True]
        self.assertTrue(host.service_restart(service_name))

        service.assert_called_with('restart', service_name)

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_pauses_a_running_systemd_unit(self, service, systemd,
                                           service_running):
        """Pause on a running systemd unit will be stopped and disabled."""
        service_name = 'foo-service'
        service_running.return_value = True
        systemd.return_value = True
        self.assertTrue(host.service_pause(service_name))
        service.assert_has_calls([
            call('stop', service_name),
            call('disable', service_name),
            call('mask', service_name)])

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_resumes_a_stopped_systemd_unit(self, service, systemd,
                                            service_running):
        """Resume on a stopped systemd unit will be started and enabled."""
        service_name = 'foo-service'
        service_running.return_value = False
        systemd.return_value = True
        self.assertTrue(host.service_resume(service_name))
        service.assert_has_calls([
            call('unmask', service_name),
            # Ensures a package starts up if disabled but not masked,
            # per lp:1692178
            call('enable', service_name),
            call('start', service_name)])

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_pauses_a_running_upstart_service(self, service, systemd,
                                              service_running):
        """Pause on a running service will call service stop."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = True
        tempdir = mkdtemp(prefix="test_pauses_an_upstart_service")
        conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
        # Just needs to exist
        with open(conf_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_pause(service_name, init_dir=tempdir))

        service.assert_called_with('stop', service_name)
        override_path = os.path.join(
            tempdir, "{}.override".format(service_name))
        with open(override_path, "r") as fh:
            override_contents = fh.read()
        self.assertEqual("manual\n", override_contents)

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_pauses_a_stopped_upstart_service(self, service, systemd,
                                              service_running):
        """Pause on a stopped service will not call service stop."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = False
        tempdir = mkdtemp(prefix="test_pauses_an_upstart_service")
        conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
        # Just needs to exist
        with open(conf_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_pause(service_name, init_dir=tempdir))

        # Stop isn't called because service is already stopped
        self.assertRaises(
            AssertionError, service.assert_called_with, 'stop', service_name)
        override_path = os.path.join(
            tempdir, "{}.override".format(service_name))
        with open(override_path, "r") as fh:
            override_contents = fh.read()
        self.assertEqual("manual\n", override_contents)

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_call')
    @patch.object(host, 'service')
    def test_pauses_a_running_sysv_service(self, service, check_call,
                                           systemd, service_running):
        """Pause calls service stop on a running sysv service."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = True
        tempdir = mkdtemp(prefix="test_pauses_a_sysv_service")
        sysv_path = os.path.join(tempdir, service_name)
        # Just needs to exist
        with open(sysv_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_pause(
            service_name, init_dir=tempdir, initd_dir=tempdir))

        service.assert_called_with('stop', service_name)
        check_call.assert_called_with(["update-rc.d", service_name, "disable"])

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_call')
    @patch.object(host, 'service')
    def test_pauses_a_stopped_sysv_service(self, service, check_call,
                                           systemd, service_running):
        """Pause does not call service stop on a stopped sysv service."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = False
        tempdir = mkdtemp(prefix="test_pauses_a_sysv_service")
        sysv_path = os.path.join(tempdir, service_name)
        # Just needs to exist
        with open(sysv_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_pause(
            service_name, init_dir=tempdir, initd_dir=tempdir))

        # Stop isn't called because service is already stopped
        self.assertRaises(
            AssertionError, service.assert_called_with, 'stop', service_name)
        check_call.assert_called_with(["update-rc.d", service_name, "disable"])

    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_pause_with_unknown_service(self, service, systemd):
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        tempdir = mkdtemp(prefix="test_pauses_with_unknown_service")
        self.addCleanup(rmtree, tempdir)
        exception = self.assertRaises(
            ValueError, host.service_pause,
            service_name, init_dir=tempdir, initd_dir=tempdir)
        self.assertIn(
            "Unable to detect {0}".format(service_name), str(exception))
        self.assertIn(tempdir, str(exception))

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_output')
    @patch.object(host, 'service')
    def test_resumes_a_running_upstart_service(self, service, check_output,
                                               systemd, service_running):
        """When the service is already running, service start isn't called."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = True
        tempdir = mkdtemp(prefix="test_resumes_an_upstart_service")
        conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
        with open(conf_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_resume(service_name, init_dir=tempdir))

        # Start isn't called because service is already running
        self.assertFalse(service.called)
        override_path = os.path.join(
            tempdir, "{}.override".format(service_name))
        self.assertFalse(os.path.exists(override_path))

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_output')
    @patch.object(host, 'service')
    def test_resumes_a_stopped_upstart_service(self, service, check_output,
                                               systemd, service_running):
        """When the service is stopped, service start is called."""
        check_output.return_value = b'foo-service stop/waiting'
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = False
        tempdir = mkdtemp(prefix="test_resumes_an_upstart_service")
        conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
        with open(conf_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_resume(service_name, init_dir=tempdir))

        service.assert_called_with('start', service_name)
        override_path = os.path.join(
            tempdir, "{}.override".format(service_name))
        self.assertFalse(os.path.exists(override_path))

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_call')
    @patch.object(host, 'service')
    def test_resumes_a_sysv_service(self, service, check_call, systemd,
                                    service_running):
        """When process is in a stop/waiting state, service start is called."""
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = False
        tempdir = mkdtemp(prefix="test_resumes_a_sysv_service")
        sysv_path = os.path.join(tempdir, service_name)
        # Just needs to exist
        with open(sysv_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_resume(
            service_name, init_dir=tempdir, initd_dir=tempdir))

        service.assert_called_with('start', service_name)
        check_call.assert_called_with(["update-rc.d", service_name, "enable"])

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_call')
    @patch.object(host, 'service')
    def test_resume_a_running_sysv_service(self, service, check_call,
                                           systemd, service_running):
        """When process is already running, service start isn't called."""
        service_name = 'foo-service'
        systemd.return_value = False
        service_running.return_value = True
        tempdir = mkdtemp(prefix="test_resumes_a_sysv_service")
        sysv_path = os.path.join(tempdir, service_name)
        # Just needs to exist
        with open(sysv_path, "w") as fh:
            fh.write("")
        self.addCleanup(rmtree, tempdir)
        self.assertTrue(host.service_resume(
            service_name, init_dir=tempdir, initd_dir=tempdir))

        # Start isn't called because service is already running
        self.assertFalse(service.called)
        check_call.assert_called_with(["update-rc.d", service_name, "enable"])

    @patch.object(host, 'service_running')
    @patch.object(host, 'init_is_systemd')
    @patch.object(host, 'service')
    def test_resume_with_unknown_service(self, service, systemd,
                                         service_running):
        service_name = 'foo-service'
        service.side_effect = [True]
        systemd.return_value = False
        service_running.return_value = False
        tempdir = mkdtemp(prefix="test_resumes_with_unknown_service")
        self.addCleanup(rmtree, tempdir)
        exception = self.assertRaises(
            ValueError, host.service_resume,
            service_name, init_dir=tempdir, initd_dir=tempdir)
        self.assertIn(
            "Unable to detect {0}".format(service_name), str(exception))
        self.assertIn(tempdir, str(exception))

    @patch.object(host, 'service')
    def test_reloads_a_service(self, service):
        service_name = 'foo-service'
        service.side_effect = [True]
        self.assertTrue(host.service_reload(service_name))

        service.assert_called_with('reload', service_name)

    @patch.object(host, 'service')
    def test_failed_reload_restarts_a_service(self, service):
        service_name = 'foo-service'
        service.side_effect = [False, True]
        self.assertTrue(
            host.service_reload(service_name, restart_on_failure=True))

        service.assert_has_calls([
            call('reload', service_name),
            call('restart', service_name)
        ])

    @patch.object(host, 'service')
    def test_failed_reload_without_restart(self, service):
        service_name = 'foo-service'
        service.side_effect = [False]
        self.assertFalse(host.service_reload(service_name))

        service.assert_called_with('reload', service_name)

    @patch.object(host, 'service')
    def test_start_a_service_fails(self, service):
        service_name = 'foo-service'
        service.side_effect = [False]
        self.assertFalse(host.service_start(service_name))

        service.assert_called_with('start', service_name)

    @patch.object(host, 'service')
    def test_stop_a_service_fails(self, service):
        service_name = 'foo-service'
        service.side_effect = [False]
        self.assertFalse(host.service_stop(service_name))

        service.assert_called_with('stop', service_name)

    @patch.object(host, 'service')
    def test_restart_a_service_fails(self, service):
        service_name = 'foo-service'
        service.side_effect = [False]
        self.assertFalse(host.service_restart(service_name))

        service.assert_called_with('restart', service_name)

    @patch.object(host, 'service')
    def test_reload_a_service_fails(self, service):
        service_name = 'foo-service'
        service.side_effect = [False]
        self.assertFalse(host.service_reload(service_name))

        service.assert_called_with('reload', service_name)

    @patch.object(host, 'service')
    def test_failed_reload_restarts_a_service_fails(self, service):
        service_name = 'foo-service'
        service.side_effect = [False, False]
        self.assertFalse(
            host.service_reload(service_name, restart_on_failure=True))

        service.assert_has_calls([
            call('reload', service_name),
            call('restart', service_name)
        ])

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_output')
    def test_service_running_on_stopped_service(self, check_output, systemd,
                                                os):
        systemd.return_value = False
        os.path.exists.return_value = True
        check_output.return_value = b'foo stop/waiting'
        self.assertFalse(host.service_running('foo'))

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_output')
    def test_service_running_on_running_service(self, check_output, systemd,
                                                os):
        systemd.return_value = False
        os.path.exists.return_value = True
        check_output.return_value = b'foo start/running, process 23871'
        self.assertTrue(host.service_running('foo'))

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.check_output')
    def test_service_running_on_unknown_service(self, check_output, systemd,
                                                os):
        systemd.return_value = False
        os.path.exists.return_value = True
        exc = subprocess.CalledProcessError(1, ['status'])
        check_output.side_effect = exc
        self.assertFalse(host.service_running('foo'))

    @patch.object(host, 'os')
    @patch.object(host, 'service')
    @patch.object(host, 'init_is_systemd')
    def test_service_systemv_running(self, systemd, service, os):
        systemd.return_value = False
        service.return_value = True
        os.path.exists.side_effect = [False, True]
        self.assertTrue(host.service_running('rabbitmq-server'))
        service.assert_called_with('status', 'rabbitmq-server')

    @patch.object(host, 'os')
    @patch.object(host, 'service')
    @patch.object(host, 'init_is_systemd')
    def test_service_systemv_not_running(self, systemd, service,
                                         os):
        systemd.return_value = False
        service.return_value = False
        os.path.exists.side_effect = [False, True]
        self.assertFalse(host.service_running('keystone'))
        service.assert_called_with('status', 'keystone')

    @patch('subprocess.call')
    @patch.object(host, 'init_is_systemd')
    def test_service_start_with_params(self, systemd, call):
        systemd.return_value = False
        call.return_value = 0
        self.assertTrue(host.service_start('ceph-osd', id=4))
        call.assert_called_with(['service', 'ceph-osd', 'start', 'id=4'])

    @patch('subprocess.call')
    @patch.object(host, 'init_is_systemd')
    def test_service_stop_with_params(self, systemd, call):
        systemd.return_value = False
        call.return_value = 0
        self.assertTrue(host.service_stop('ceph-osd', id=4))
        call.assert_called_with(['service', 'ceph-osd', 'stop', 'id=4'])

    @patch('subprocess.call')
    @patch.object(host, 'init_is_systemd')
    def test_service_start_systemd_with_params(self, systemd, call):
        systemd.return_value = True
        call.return_value = 0
        self.assertTrue(host.service_start('ceph-osd', id=4))
        call.assert_called_with(['systemctl', 'start', 'ceph-osd'])

    @patch('grp.getgrnam')
    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adds_a_user_if_it_doesnt_exist(self, log, check_call,
                                            getpwnam, getgrnam):
        username = 'johndoe'
        password = 'eodnhoj'
        shell = '/bin/bash'
        existing_user_pwnam = KeyError('user not found')
        new_user_pwnam = 'some user pwnam'

        getpwnam.side_effect = [existing_user_pwnam, new_user_pwnam]

        result = host.adduser(username, password=password)

        self.assertEqual(result, new_user_pwnam)
        check_call.assert_called_with([
            'useradd',
            '--create-home',
            '--shell', shell,
            '--password', password,
            '-g', username,
            username
        ])
        getpwnam.assert_called_with(username)

    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_doesnt_add_user_if_it_already_exists(self, log, check_call,
                                                  getpwnam):
        username = 'johndoe'
        password = 'eodnhoj'
        existing_user_pwnam = 'some user pwnam'

        getpwnam.return_value = existing_user_pwnam

        result = host.adduser(username, password=password)

        self.assertEqual(result, existing_user_pwnam)
        self.assertFalse(check_call.called)
        getpwnam.assert_called_with(username)

    @patch('grp.getgrnam')
    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adds_a_user_with_different_shell(self, log, check_call, getpwnam,
                                              getgrnam):
        username = 'johndoe'
        password = 'eodnhoj'
        shell = '/bin/zsh'
        existing_user_pwnam = KeyError('user not found')
        new_user_pwnam = 'some user pwnam'

        getpwnam.side_effect = [existing_user_pwnam, new_user_pwnam]
        getgrnam.side_effect = KeyError('group not found')

        result = host.adduser(username, password=password, shell=shell)

        self.assertEqual(result, new_user_pwnam)
        check_call.assert_called_with([
            'useradd',
            '--create-home',
            '--shell', shell,
            '--password', password,
            username
        ])
        getpwnam.assert_called_with(username)

    @patch('grp.getgrnam')
    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adduser_with_groups(self, log, check_call, getpwnam, getgrnam):
        username = 'johndoe'
        password = 'eodnhoj'
        shell = '/bin/bash'
        existing_user_pwnam = KeyError('user not found')
        new_user_pwnam = 'some user pwnam'

        getpwnam.side_effect = [existing_user_pwnam, new_user_pwnam]

        result = host.adduser(username, password=password,
                              primary_group='foo', secondary_groups=[
                                  'bar', 'qux',
                              ])

        self.assertEqual(result, new_user_pwnam)
        check_call.assert_called_with([
            'useradd',
            '--create-home',
            '--shell', shell,
            '--password', password,
            '-g', 'foo',
            '-G', 'bar,qux',
            username
        ])
        getpwnam.assert_called_with(username)
        assert not getgrnam.called

    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adds_a_systemuser(self, log, check_call, getpwnam):
        username = 'johndoe'
        existing_user_pwnam = KeyError('user not found')
        new_user_pwnam = 'some user pwnam'

        getpwnam.side_effect = [existing_user_pwnam, new_user_pwnam]

        result = host.adduser(username, system_user=True)

        self.assertEqual(result, new_user_pwnam)
        check_call.assert_called_with([
            'useradd',
            '--system',
            username
        ])
        getpwnam.assert_called_with(username)

    @patch('pwd.getpwnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adds_a_systemuser_with_home_dir(self, log, check_call, getpwnam):
        username = 'johndoe'
        existing_user_pwnam = KeyError('user not found')
        new_user_pwnam = 'some user pwnam'

        getpwnam.side_effect = [existing_user_pwnam, new_user_pwnam]

        result = host.adduser(username, system_user=True,
                              home_dir='/var/lib/johndoe')

        self.assertEqual(result, new_user_pwnam)
        check_call.assert_called_with([
            'useradd',
            '--home',
            '/var/lib/johndoe',
            '--system',
            username
        ])
        getpwnam.assert_called_with(username)

    @patch('pwd.getpwnam')
    @patch('pwd.getpwuid')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_add_user_uid(self, log, check_call, getgrnam, getpwuid, getpwnam):
        user_name = 'james'
        user_id = 1111
        uid_key_error = KeyError('user not found')
        getpwuid.side_effect = uid_key_error
        host.adduser(user_name, uid=user_id)

        check_call.assert_called_with([
            'useradd',
            '--uid',
            str(user_id),
            '--system',
            '-g',
            user_name,
            user_name
        ])
        getpwnam.assert_called_with(user_name)
        getpwuid.assert_called_with(user_id)

    @patch('grp.getgrnam')
    @patch('grp.getgrgid')
    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_add_group_gid(self, log, check_call, getgrgid, getgrnam):
        group_name = 'darkhorse'
        group_id = 1005
        existing_group_gid = KeyError('group not found')
        new_group_gid = 1006
        getgrgid.side_effect = [existing_group_gid, new_group_gid]

        host.add_group(group_name, gid=group_id)
        check_call.assert_called_with([
            'addgroup',
            '--gid',
            str(group_id),
            '--group',
            group_name
        ])
        getgrgid.assert_called_with(group_id)
        getgrnam.assert_called_with(group_name)

    @patch('pwd.getpwnam')
    def test_user_exists_true(self, getpwnam):
        getpwnam.side_effect = 'pw info'
        self.assertTrue(host.user_exists('bob'))

    @patch('pwd.getpwnam')
    def test_user_exists_false(self, getpwnam):
        getpwnam.side_effect = KeyError('user not found')
        self.assertFalse(host.user_exists('bob'))

    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_adds_a_user_to_a_group(self, log, check_call):
        username = 'foo'
        group = 'bar'

        host.add_user_to_group(username, group)

        check_call.assert_called_with([
            'gpasswd', '-a',
            username,
            group
        ])

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_add_a_group_if_it_doesnt_exist_ubuntu(self, check_call,
                                                   getgrnam, platform):
        platform.return_value = 'ubuntu'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = KeyError('group not found')
        new_group_grnam = 'some group grnam'

        getgrnam.side_effect = [existing_group_grnam, new_group_grnam]
        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name)

        self.assertEqual(result, new_group_grnam)
        check_call.assert_called_with(['addgroup', '--group', group_name])
        getgrnam.assert_called_with(group_name)

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_add_a_group_if_it_doesnt_exist_centos(self, check_call,
                                                   getgrnam, platform):
        platform.return_value = 'centos'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = KeyError('group not found')
        new_group_grnam = 'some group grnam'

        getgrnam.side_effect = [existing_group_grnam, new_group_grnam]

        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name)

        self.assertEqual(result, new_group_grnam)
        check_call.assert_called_with(['groupadd', group_name])
        getgrnam.assert_called_with(group_name)

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_doesnt_add_group_if_it_already_exists_ubuntu(self, check_call,
                                                          getgrnam, platform):
        platform.return_value = 'ubuntu'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = 'some group grnam'

        getgrnam.return_value = existing_group_grnam

        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name)

        self.assertEqual(result, existing_group_grnam)
        self.assertFalse(check_call.called)
        getgrnam.assert_called_with(group_name)

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_doesnt_add_group_if_it_already_exists_centos(self, check_call,
                                                          getgrnam, platform):
        platform.return_value = 'centos'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = 'some group grnam'

        getgrnam.return_value = existing_group_grnam

        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name)

        self.assertEqual(result, existing_group_grnam)
        self.assertFalse(check_call.called)
        getgrnam.assert_called_with(group_name)

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_add_a_system_group_ubuntu(self, check_call, getgrnam, platform):
        platform.return_value = 'ubuntu'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = KeyError('group not found')
        new_group_grnam = 'some group grnam'

        getgrnam.side_effect = [existing_group_grnam, new_group_grnam]

        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name, system_group=True)

        self.assertEqual(result, new_group_grnam)
        check_call.assert_called_with([
            'addgroup',
            '--system',
            group_name
        ])
        getgrnam.assert_called_with(group_name)

    @patch.object(osplatform, 'get_platform')
    @patch('grp.getgrnam')
    @patch('subprocess.check_call')
    def test_add_a_system_group_centos(self, check_call, getgrnam, platform):
        platform.return_value = 'centos'
        imp.reload(host)

        group_name = 'testgroup'
        existing_group_grnam = KeyError('group not found')
        new_group_grnam = 'some group grnam'

        getgrnam.side_effect = [existing_group_grnam, new_group_grnam]

        with patch("charmhelpers.core.host.log"):
            result = host.add_group(group_name, system_group=True)

        self.assertEqual(result, new_group_grnam)
        check_call.assert_called_with([
            'groupadd',
            '-r',
            group_name
        ])
        getgrnam.assert_called_with(group_name)

    @patch('subprocess.check_call')
    def test_chage_no_chroot(self, check_call):
        host.chage('usera', expiredate='2019-09-28', maxdays='11')
        check_call.assert_called_with([
            'chage',
            '--expiredate', '2019-09-28',
            '--maxdays', '11',
            'usera'
        ])

    @patch('subprocess.check_call')
    def test_chage_chroot(self, check_call):
        host.chage('usera', expiredate='2019-09-28', maxdays='11',
                   root='mychroot')
        check_call.assert_called_with([
            'chage',
            '--root', 'mychroot',
            '--expiredate', '2019-09-28',
            '--maxdays', '11',
            'usera'
        ])

    @patch('subprocess.check_call')
    def test_remove_password_expiry(self, check_call):
        host.remove_password_expiry('usera')
        check_call.assert_called_with([
            'chage',
            '--expiredate', '-1',
            '--inactive', '-1',
            '--mindays', '0',
            '--maxdays', '-1',
            'usera'
        ])

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_rsyncs_a_path(self, log, check_output):
        from_path = '/from/this/path/foo'
        to_path = '/to/this/path/bar'
        check_output.return_value = b' some output '  # Spaces will be stripped

        result = host.rsync(from_path, to_path)

        self.assertEqual(result, 'some output')
        check_output.assert_called_with(['/usr/bin/rsync', '-r', '--delete',
                                         '--executability',
                                         '/from/this/path/foo',
                                         '/to/this/path/bar'], stderr=subprocess.STDOUT)

    @patch('subprocess.check_call')
    @patch.object(host, 'log')
    def test_creates_a_symlink(self, log, check_call):
        source = '/from/this/path/foo'
        destination = '/to/this/path/bar'

        host.symlink(source, destination)

        check_call.assert_called_with(['ln', '-sf',
                                       '/from/this/path/foo',
                                       '/to/this/path/bar'])

    @patch('pwd.getpwnam')
    @patch('grp.getgrnam')
    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_creates_a_directory_if_it_doesnt_exist(self, os_, log,
                                                    getgrnam, getpwnam):
        uid = 123
        gid = 234
        owner = 'some-user'
        group = 'some-group'
        path = '/some/other/path/from/link'
        realpath = '/some/path'
        path_exists = False
        perms = 0o644

        getpwnam.return_value.pw_uid = uid
        getgrnam.return_value.gr_gid = gid
        os_.path.abspath.return_value = realpath
        os_.path.exists.return_value = path_exists

        host.mkdir(path, owner=owner, group=group, perms=perms)

        getpwnam.assert_called_with('some-user')
        getgrnam.assert_called_with('some-group')
        os_.path.abspath.assert_called_with(path)
        os_.path.exists.assert_called_with(realpath)
        os_.makedirs.assert_called_with(realpath, perms)
        os_.chown.assert_called_with(realpath, uid, gid)

    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_creates_a_directory_with_defaults(self, os_, log):
        uid = 0
        gid = 0
        path = '/some/other/path/from/link'
        realpath = '/some/path'
        path_exists = False
        perms = 0o555

        os_.path.abspath.return_value = realpath
        os_.path.exists.return_value = path_exists

        host.mkdir(path)

        os_.path.abspath.assert_called_with(path)
        os_.path.exists.assert_called_with(realpath)
        os_.makedirs.assert_called_with(realpath, perms)
        os_.chown.assert_called_with(realpath, uid, gid)

    @patch('pwd.getpwnam')
    @patch('grp.getgrnam')
    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_removes_file_with_same_path_before_mkdir(self, os_, log,
                                                      getgrnam, getpwnam):
        uid = 123
        gid = 234
        owner = 'some-user'
        group = 'some-group'
        path = '/some/other/path/from/link'
        realpath = '/some/path'
        path_exists = True
        force = True
        is_dir = False
        perms = 0o644

        getpwnam.return_value.pw_uid = uid
        getgrnam.return_value.gr_gid = gid
        os_.path.abspath.return_value = realpath
        os_.path.exists.return_value = path_exists
        os_.path.isdir.return_value = is_dir

        host.mkdir(path, owner=owner, group=group, perms=perms, force=force)

        getpwnam.assert_called_with('some-user')
        getgrnam.assert_called_with('some-group')
        os_.path.abspath.assert_called_with(path)
        os_.path.exists.assert_called_with(realpath)
        os_.unlink.assert_called_with(realpath)
        os_.makedirs.assert_called_with(realpath, perms)
        os_.chown.assert_called_with(realpath, uid, gid)

    @patch('pwd.getpwnam')
    @patch('grp.getgrnam')
    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_writes_content_to_a_file(self, os_, log, getgrnam, getpwnam):
        # Curly brackets here demonstrate that we are *not* rendering
        # these strings with Python's string formatting. This is a
        # change from the original behavior per Bug #1195634.
        uid = 123
        gid = 234
        owner = 'some-user-{foo}'
        group = 'some-group-{bar}'
        path = '/some/path/{baz}'
        contents = b'what is {juju}'
        perms = 0o644
        fileno = 'some-fileno'

        getpwnam.return_value.pw_uid = uid
        getgrnam.return_value.gr_gid = gid

        with patch_open() as (mock_open, mock_file):
            mock_file.fileno.return_value = fileno

            host.write_file(path, contents, owner=owner, group=group,
                            perms=perms)

            getpwnam.assert_called_with('some-user-{foo}')
            getgrnam.assert_called_with('some-group-{bar}')
            mock_open.assert_called_with('/some/path/{baz}', 'wb')
            os_.fchown.assert_called_with(fileno, uid, gid)
            os_.fchmod.assert_called_with(fileno, perms)
            mock_file.write.assert_called_with(b'what is {juju}')

    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_writes_content_with_default(self, os_, log):
        uid = 0
        gid = 0
        path = '/some/path/{baz}'
        fmtstr = b'what is {juju}'
        perms = 0o444
        fileno = 'some-fileno'

        with patch_open() as (mock_open, mock_file):
            mock_file.fileno.return_value = fileno

            host.write_file(path, fmtstr)

            mock_open.assert_called_with('/some/path/{baz}', 'wb')
            os_.fchown.assert_called_with(fileno, uid, gid)
            os_.fchmod.assert_called_with(fileno, perms)
            mock_file.write.assert_called_with(b'what is {juju}')

    @patch.object(host, 'log')
    @patch.object(host, 'os')
    def test_writes_binary_contents(self, os_, log):
        path = '/some/path/{baz}'
        fmtstr = six.u('what is {juju}\N{TRADE MARK SIGN}').encode('UTF-8')
        fileno = 'some-fileno'

        with patch_open() as (mock_open, mock_file):
            mock_file.fileno.return_value = fileno

            host.write_file(path, fmtstr)

            mock_open.assert_called_with('/some/path/{baz}', 'wb')
            mock_file.write.assert_called_with(fmtstr)

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_mounts_a_device(self, log, check_output):
        device = '/dev/guido'
        mountpoint = '/mnt/guido'
        options = 'foo,bar'

        result = host.mount(device, mountpoint, options)

        self.assertTrue(result)
        check_output.assert_called_with(['mount', '-o', 'foo,bar',
                                         '/dev/guido', '/mnt/guido'])

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_doesnt_mount_on_error(self, log, check_output):
        device = '/dev/guido'
        mountpoint = '/mnt/guido'
        options = 'foo,bar'

        error = subprocess.CalledProcessError(123, 'mount it', 'Oops...')
        check_output.side_effect = error

        result = host.mount(device, mountpoint, options)

        self.assertFalse(result)
        check_output.assert_called_with(['mount', '-o', 'foo,bar',
                                         '/dev/guido', '/mnt/guido'])

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_mounts_a_device_without_options(self, log, check_output):
        device = '/dev/guido'
        mountpoint = '/mnt/guido'

        result = host.mount(device, mountpoint)

        self.assertTrue(result)
        check_output.assert_called_with(['mount', '/dev/guido', '/mnt/guido'])

    @patch.object(host, 'Fstab')
    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_mounts_and_persist_a_device(self, log, check_output, fstab):
        """Check if a mount works with the persist flag set to True
        """
        device = '/dev/guido'
        mountpoint = '/mnt/guido'
        options = 'foo,bar'

        result = host.mount(device, mountpoint, options, persist=True)

        self.assertTrue(result)
        check_output.assert_called_with(['mount', '-o', 'foo,bar',
                                         '/dev/guido', '/mnt/guido'])

        fstab.add.assert_called_with('/dev/guido', '/mnt/guido', 'ext3',
                                     options='foo,bar')

        result = host.mount(device, mountpoint, options, persist=True,
                            filesystem="xfs")

        self.assertTrue(result)
        fstab.add.assert_called_with('/dev/guido', '/mnt/guido', 'xfs',
                                     options='foo,bar')

    @patch.object(host, 'Fstab')
    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_umounts_a_device(self, log, check_output, fstab):
        mountpoint = '/mnt/guido'

        result = host.umount(mountpoint, persist=True)

        self.assertTrue(result)
        check_output.assert_called_with(['umount', mountpoint])
        fstab.remove_by_mountpoint_called_with(mountpoint)

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_umounts_and_persist_device(self, log, check_output):
        mountpoint = '/mnt/guido'

        result = host.umount(mountpoint)

        self.assertTrue(result)
        check_output.assert_called_with(['umount', '/mnt/guido'])

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_doesnt_umount_on_error(self, log, check_output):
        mountpoint = '/mnt/guido'

        error = subprocess.CalledProcessError(123, 'mount it', 'Oops...')
        check_output.side_effect = error

        result = host.umount(mountpoint)

        self.assertFalse(result)
        check_output.assert_called_with(['umount', '/mnt/guido'])

    def test_lists_the_mount_points(self):
        with patch_open() as (mock_open, mock_file):
            mock_file.readlines.return_value = MOUNT_LINES
            result = host.mounts()

            self.assertEqual(result, [
                ['/', 'rootfs'],
                ['/sys', 'sysfs'],
                ['/proc', 'proc'],
                ['/dev', 'udev'],
                ['/dev/pts', 'devpts']
            ])
            mock_open.assert_called_with('/proc/mounts')

    _hash_files = {
        '/etc/exists.conf': 'lots of nice ceph configuration',
        '/etc/missing.conf': None
    }

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_fstab_mount(self, log, check_output):
        self.assertTrue(host.fstab_mount('/mnt/mymntpnt'))
        check_output.assert_called_with(['mount', '/mnt/mymntpnt'])

    @patch('subprocess.check_output')
    @patch.object(host, 'log')
    def test_fstab_mount_fail(self, log, check_output):
        error = subprocess.CalledProcessError(123, 'mount it', 'Oops...')
        check_output.side_effect = error
        self.assertFalse(host.fstab_mount('/mnt/mymntpnt'))
        check_output.assert_called_with(['mount', '/mnt/mymntpnt'])

    @patch('hashlib.md5')
    @patch('os.path.exists')
    def test_file_hash_exists(self, exists, md5):
        filename = '/etc/exists.conf'
        exists.side_effect = [True]
        m = md5()
        m.hexdigest.return_value = self._hash_files[filename]
        with patch_open() as (mock_open, mock_file):
            mock_file.read.return_value = self._hash_files[filename]
            result = host.file_hash(filename)
            self.assertEqual(result, self._hash_files[filename])

    @patch('os.path.exists')
    def test_file_hash_missing(self, exists):
        filename = '/etc/missing.conf'
        exists.side_effect = [False]
        with patch_open() as (mock_open, mock_file):
            mock_file.read.return_value = self._hash_files[filename]
            result = host.file_hash(filename)
            self.assertEqual(result, None)

    @patch('hashlib.sha1')
    @patch('os.path.exists')
    def test_file_hash_sha1(self, exists, sha1):
        filename = '/etc/exists.conf'
        exists.side_effect = [True]
        m = sha1()
        m.hexdigest.return_value = self._hash_files[filename]
        with patch_open() as (mock_open, mock_file):
            mock_file.read.return_value = self._hash_files[filename]
            result = host.file_hash(filename, hash_type='sha1')
            self.assertEqual(result, self._hash_files[filename])

    @patch.object(host, 'file_hash')
    def test_check_hash(self, file_hash):
        file_hash.return_value = 'good-hash'
        self.assertRaises(host.ChecksumError, host.check_hash,
                          'file', 'bad-hash')
        host.check_hash('file', 'good-hash', 'sha256')
        self.assertEqual(file_hash.call_args_list, [
            call('file', 'md5'),
            call('file', 'sha256'),
        ])

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_restart_no_changes(self, iglob, exists, service):
        file_name = '/etc/missing.conf'
        restart_map = {
            file_name: ['test-service']
        }
        iglob.return_value = []

        @host.restart_on_change(restart_map)
        def make_no_changes():
            pass

        make_no_changes()

        assert not service.called
        assert not exists.called

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_restart_on_change(self, iglob, exists, service):
        file_name = '/etc/missing.conf'
        restart_map = {
            file_name: ['test-service']
        }
        iglob.side_effect = [[], [file_name]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes(mock_file):
            mock_file.read.return_value = b"newstuff"

        with patch_open() as (mock_open, mock_file):
            make_some_changes(mock_file)

        for service_name in restart_map[file_name]:
            service.assert_called_with('restart', service_name)

        exists.assert_has_calls([
            call(file_name),
        ])

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_multiservice_restart_on_change(self, iglob, exists, service):
        file_name_one = '/etc/missing.conf'
        file_name_two = '/etc/exists.conf'
        restart_map = {
            file_name_one: ['test-service'],
            file_name_two: ['test-service', 'test-service2']
        }
        iglob.side_effect = [[], [file_name_two],
                             [file_name_one], [file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'exists', b'missing', b'exists2']
            make_some_changes()

        # Restart should only happen once per service
        for svc in ['test-service2', 'test-service']:
            c = call('restart', svc)
            self.assertEquals(1, service.call_args_list.count(c))

        exists.assert_has_calls([
            call(file_name_one),
            call(file_name_two)
        ])

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_multiservice_restart_on_change_in_order(self, iglob, exists,
                                                     service):
        file_name_one = '/etc/cinder/cinder.conf'
        file_name_two = '/etc/haproxy/haproxy.conf'
        restart_map = OrderedDict([
            (file_name_one, ['some-api']),
            (file_name_two, ['haproxy'])
        ])
        iglob.side_effect = [[], [file_name_two],
                             [file_name_one], [file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'exists', b'missing', b'exists2']
            make_some_changes()

        # Restarts should happen in the order they are described in the
        # restart map.
        expected = [
            call('restart', 'some-api'),
            call('restart', 'haproxy')
        ]
        self.assertEquals(expected, service.call_args_list)

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_glob_no_restart(self, iglob, exists, service):
        glob_path = '/etc/service/*.conf'
        file_name_one = '/etc/service/exists.conf'
        file_name_two = '/etc/service/exists2.conf'
        restart_map = {
            glob_path: ['service']
        }
        iglob.side_effect = [[file_name_one, file_name_two],
                             [file_name_one, file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'content', b'content2',
                                          b'content', b'content2']
            make_some_changes()

        self.assertEquals([], service.call_args_list)

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_glob_restart_on_change(self, iglob, exists, service):
        glob_path = '/etc/service/*.conf'
        file_name_one = '/etc/service/exists.conf'
        file_name_two = '/etc/service/exists2.conf'
        restart_map = {
            glob_path: ['service']
        }
        iglob.side_effect = [[file_name_one, file_name_two],
                             [file_name_one, file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'content', b'content2',
                                          b'changed', b'content2']
            make_some_changes()

        self.assertEquals([call('restart', 'service')], service.call_args_list)

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_glob_restart_on_create(self, iglob, exists, service):
        glob_path = '/etc/service/*.conf'
        file_name_one = '/etc/service/exists.conf'
        file_name_two = '/etc/service/missing.conf'
        restart_map = {
            glob_path: ['service']
        }
        iglob.side_effect = [[file_name_one],
                             [file_name_one, file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'exists',
                                          b'exists', b'created']
            make_some_changes()

        self.assertEquals([call('restart', 'service')], service.call_args_list)

    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_glob_restart_on_delete(self, iglob, exists, service):
        glob_path = '/etc/service/*.conf'
        file_name_one = '/etc/service/exists.conf'
        file_name_two = '/etc/service/exists2.conf'
        restart_map = {
            glob_path: ['service']
        }
        iglob.side_effect = [[file_name_one, file_name_two],
                             [file_name_two]]
        exists.return_value = True

        @host.restart_on_change(restart_map)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'exists', b'exists2',
                                          b'exists2']
            make_some_changes()

        self.assertEquals([call('restart', 'service')], service.call_args_list)

    @patch.object(host, 'service_reload')
    @patch.object(host, 'service')
    @patch('os.path.exists')
    @patch('glob.iglob')
    def test_restart_on_change_restart_functs(self, iglob, exists, service,
                                              service_reload):
        file_name_one = '/etc/cinder/cinder.conf'
        file_name_two = '/etc/haproxy/haproxy.conf'
        restart_map = OrderedDict([
            (file_name_one, ['some-api']),
            (file_name_two, ['haproxy'])
        ])
        iglob.side_effect = [[], [file_name_two],
                             [file_name_one], [file_name_two]]
        exists.return_value = True

        restart_funcs = {
            'some-api': service_reload,
        }

        @host.restart_on_change(restart_map, restart_functions=restart_funcs)
        def make_some_changes():
            pass

        with patch_open() as (mock_open, mock_file):
            mock_file.read.side_effect = [b'exists', b'missing', b'exists2']
            make_some_changes()

        self.assertEquals([call('restart', 'haproxy')], service.call_args_list)
        self.assertEquals([call('some-api')], service_reload.call_args_list)

    @patch.object(osplatform, 'get_platform')
    def test_lsb_release_ubuntu(self, platform):
        platform.return_value = 'ubuntu'
        imp.reload(host)

        result = {
            "DISTRIB_ID": "Ubuntu",
            "DISTRIB_RELEASE": "13.10",
            "DISTRIB_CODENAME": "saucy",
            "DISTRIB_DESCRIPTION": "\"Ubuntu Saucy Salamander "
                                   "(development branch)\""
        }
        with mocked_open('/etc/lsb-release', LSB_RELEASE):
            lsb_release = host.lsb_release()
            for key in result:
                self.assertEqual(result[key], lsb_release[key])

    @patch.object(osplatform, 'get_platform')
    def test_lsb_release_centos(self, platform):
        platform.return_value = 'centos'
        imp.reload(host)

        result = {
            'NAME': '"CentOS Linux"',
            'ANSI_COLOR': '"0;31"',
            'ID_LIKE': '"rhel fedora"',
            'VERSION_ID': '"7"',
            'BUG_REPORT_URL': '"https://bugs.centos.org/"',
            'CENTOS_MANTISBT_PROJECT': '"CentOS-7"',
            'PRETTY_NAME': '"CentOS Linux 7 (Core)"',
            'VERSION': '"7 (Core)"',
            'REDHAT_SUPPORT_PRODUCT_VERSION': '"7"',
            'CENTOS_MANTISBT_PROJECT_VERSION': '"7"',
            'REDHAT_SUPPORT_PRODUCT': '"centos"',
            'HOME_URL': '"https://www.centos.org/"',
            'CPE_NAME': '"cpe:/o:centos:centos:7"',
            'ID': '"centos"'
        }
        with mocked_open('/etc/os-release', OS_RELEASE):
            lsb_release = host.lsb_release()
            for key in result:
                self.assertEqual(result[key], lsb_release[key])

    def test_pwgen(self):
        pw = host.pwgen()
        self.assert_(len(pw) >= 35, 'Password is too short')

        pw = host.pwgen(10)
        self.assertEqual(len(pw), 10, 'Password incorrect length')

        pw2 = host.pwgen(10)
        self.assertNotEqual(pw, pw2, 'Duplicated password')

    @patch.object(host, 'glob')
    @patch('os.path.realpath')
    @patch('os.path.isdir')
    def test_is_phy_iface(self, mock_isdir, mock_realpath, mock_glob):
        mock_isdir.return_value = True
        mock_glob.glob.return_value = ['/sys/class/net/eth0',
                                       '/sys/class/net/veth0']

        def fake_realpath(soft):
            if soft.endswith('/eth0'):
                hard = ('/sys/devices/pci0000:00/0000:00:1c.4'
                        '/0000:02:00.1/net/eth0')
            else:
                hard = '/sys/devices/virtual/net/veth0'

            return hard

        mock_realpath.side_effect = fake_realpath
        self.assertTrue(host.is_phy_iface('eth0'))
        self.assertFalse(host.is_phy_iface('veth0'))

    @patch('os.path.exists')
    @patch('os.path.realpath')
    @patch('os.path.isdir')
    def test_get_bond_master(self, mock_isdir, mock_realpath, mock_exists):
        mock_isdir.return_value = True

        def fake_realpath(soft):
            if soft.endswith('/eth0'):
                return ('/sys/devices/pci0000:00/0000:00:1c.4'
                        '/0000:02:00.1/net/eth0')
            elif soft.endswith('/br0'):
                return '/sys/devices/virtual/net/br0'
            elif soft.endswith('/master'):
                return '/sys/devices/virtual/net/bond0'

            return None

        def fake_exists(path):
            return True

        mock_exists.side_effect = fake_exists
        mock_realpath.side_effect = fake_realpath
        self.assertEqual(host.get_bond_master('eth0'), 'bond0')
        self.assertIsNone(host.get_bond_master('br0'))

    @patch('subprocess.check_output')
    def test_list_nics(self, check_output):
        check_output.return_value = IP_LINES
        nics = host.list_nics()
        self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])
        nics = host.list_nics('eth')
        self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])
        nics = host.list_nics(['eth'])
        self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])

    @patch('subprocess.check_output')
    def test_list_nics_with_bonds(self, check_output):
        check_output.return_value = IP_LINE_BONDS
        nics = host.list_nics('bond')
        self.assertEqual(nics, ['bond0.10', ])

    @patch('subprocess.check_output')
    def test_get_nic_mtu_with_bonds(self, check_output):
        check_output.return_value = IP_LINE_BONDS
        nic = "bond0.10"
        mtu = host.get_nic_mtu(nic)
        self.assertEqual(mtu, '1500')

    @patch('subprocess.check_call')
    def test_set_nic_mtu(self, mock_call):
        mock_call.return_value = 0
        nic = 'eth7'
        mtu = '1546'
        host.set_nic_mtu(nic, mtu)
        mock_call.assert_called_with(['ip', 'link', 'set', nic, 'mtu', mtu])

    @patch('subprocess.check_output')
    def test_get_nic_mtu(self, check_output):
        check_output.return_value = IP_LINE_ETH0
        nic = "eth0"
        mtu = host.get_nic_mtu(nic)
        self.assertEqual(mtu, '1500')

    @patch('subprocess.check_output')
    def test_get_nic_mtu_vlan(self, check_output):
        check_output.return_value = IP_LINE_ETH0_VLAN
        nic = "eth0.10"
        mtu = host.get_nic_mtu(nic)
        self.assertEqual(mtu, '1500')

    @patch('subprocess.check_output')
    def test_get_nic_hwaddr(self, check_output):
        check_output.return_value = IP_LINE_HWADDR
        nic = "eth0"
        hwaddr = host.get_nic_hwaddr(nic)
        self.assertEqual(hwaddr, 'e4:11:5b:ab:a7:3c')

    @patch.object(osplatform, 'get_platform')
    @patch.object(apt_pkg, 'Cache')
    def test_cmp_pkgrevno_revnos_ubuntu(self, pkg_cache, platform):
        platform.return_value = 'ubuntu'
        imp.reload(host)

        class MockPackage:
            class MockPackageRevno:
                def __init__(self, ver_str):
                    self.ver_str = ver_str

            def __init__(self, current_ver):
                self.current_ver = self.MockPackageRevno(current_ver)

        pkg_dict = {
            'python': MockPackage('2.4')
        }
        pkg_cache.return_value = pkg_dict
        self.assertEqual(host.cmp_pkgrevno('python', '2.3'), 1)
        self.assertEqual(host.cmp_pkgrevno('python', '2.4'), 0)
        self.assertEqual(host.cmp_pkgrevno('python', '2.5'), -1)

    @patch.object(osplatform, 'get_platform')
    def test_cmp_pkgrevno_revnos_centos(self, platform):
        platform.return_value = 'centos'
        imp.reload(host)

        class MockPackage:
            def __init__(self, name, version):
                self.Name = name
                self.version = version

        yum_dict = {
            'installed': {
                MockPackage('python', '2.4')
            }
        }

        import yum
        yum.YumBase.return_value.doPackageLists.return_value = (
            yum_dict)

        self.assertEqual(host.cmp_pkgrevno('python', '2.3'), 1)
        self.assertEqual(host.cmp_pkgrevno('python', '2.4'), 0)
        self.assertEqual(host.cmp_pkgrevno('python', '2.5'), -1)

    @patch.object(host.os, 'stat')
    @patch.object(host.pwd, 'getpwuid')
    @patch.object(host.grp, 'getgrgid')
    @patch('posix.stat_result')
    def test_owner(self, stat_result_, getgrgid_, getpwuid_, stat_):
        getgrgid_.return_value = ['testgrp']
        getpwuid_.return_value = ['testuser']
        stat_.return_value = stat_result_()

        user, group = host.owner('/some/path')
        stat_.assert_called_once_with('/some/path')
        self.assertEqual('testuser', user)
        self.assertEqual('testgrp', group)

    def test_get_total_ram(self):
        raw = dedent('''\
                     MemFree:          183868 kB
                     MemTotal:        7096108 kB
                     MemAvailable:    5645240 kB
                     ''').strip()
        with patch_open() as (mock_open, mock_file):
            mock_file.readlines.return_value = raw.splitlines()
            self.assertEqual(host.get_total_ram(), 7266414592)  # 7GB
            mock_open.assert_called_once_with('/proc/meminfo', 'r')

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_is_container_with_systemd_container(self,
                                                 call,
                                                 init_is_systemd,
                                                 mock_os):
        init_is_systemd.return_value = True
        call.return_value = 0
        self.assertTrue(host.is_container())
        call.assert_called_with(['systemd-detect-virt', '--container'])

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_is_container_with_systemd_non_container(self,
                                                     call,
                                                     init_is_systemd,
                                                     mock_os):
        init_is_systemd.return_value = True
        call.return_value = 1
        self.assertFalse(host.is_container())
        call.assert_called_with(['systemd-detect-virt', '--container'])

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_is_container_with_upstart_container(self,
                                                 call,
                                                 init_is_systemd,
                                                 mock_os):
        init_is_systemd.return_value = False
        mock_os.path.exists.return_value = True
        self.assertTrue(host.is_container())
        mock_os.path.exists.assert_called_with('/run/container_type')

    @patch.object(host, 'os')
    @patch.object(host, 'init_is_systemd')
    @patch('subprocess.call')
    def test_is_container_with_upstart_not_container(self,
                                                     call,
                                                     init_is_systemd,
                                                     mock_os):
        init_is_systemd.return_value = False
        mock_os.path.exists.return_value = False
        self.assertFalse(host.is_container())
        mock_os.path.exists.assert_called_with('/run/container_type')

    def test_updatedb(self):
        updatedb_text = 'PRUNEPATHS="/tmp"'
        self.assertEqual(host.updatedb(updatedb_text, '/srv/node'),
                         'PRUNEPATHS="/tmp /srv/node"')

    def test_no_change_updatedb(self):
        updatedb_text = 'PRUNEPATHS="/tmp /srv/node"'
        self.assertEqual(host.updatedb(updatedb_text, '/srv/node'),
                         updatedb_text)

    def test_no_prunepaths(self):
        updatedb_text = 'PRUNE_BIND_MOUNTS="yes"'
        self.assertEqual(host.updatedb(updatedb_text, '/srv/node'),
                         updatedb_text)

    def test_write_updatedb(self):
        _open = mock_open(read_data='PRUNEPATHS="/tmp /srv/node"')
        with patch('charmhelpers.core.host.open', _open, create=True):
            host.add_to_updatedb_prunepath("/tmp/test")
        handle = _open()

        self.assertTrue(handle.read.call_count == 1)
        self.assertTrue(handle.seek.call_count == 1)
        handle.write.assert_called_once_with(
            'PRUNEPATHS="/tmp /srv/node /tmp/test"')

    @patch.object(host, 'local_unit')
    def test_modulo_distribution(self, local_unit):
        local_unit.return_value = 'test/7'

        # unit % modulo * wait
        self.assertEqual(host.modulo_distribution(modulo=6, wait=10), 10)

        # Zero wait when unit % modulo == 0
        self.assertEqual(host.modulo_distribution(modulo=7, wait=10), 0)

        # modulo * wait when unit % modulo == 0 and non_zero_wait=True
        self.assertEqual(host.modulo_distribution(modulo=7, wait=10,
                                                  non_zero_wait=True),
                         70)


class TestHostCompator(TestCase):

    def test_compare_ubuntu_releases(self):
        from charmhelpers.osplatform import get_platform
        if get_platform() == 'ubuntu':
            self.assertTrue(host.CompareHostReleases('yakkety') < 'zesty')
