From 557c25a794675d81e15d653edf92c8f1b2e36cf1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 25 Aug 2013 00:49:15 +0900 Subject: [PATCH 1/8] Add a hostname module, which sets system's hostname. --- system/hostname | 247 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 system/hostname diff --git a/system/hostname b/system/hostname new file mode 100644 index 00000000000..76991edf63b --- /dev/null +++ b/system/hostname @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2013, Hiroaki Nakamura +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: hostname +author: Hiroaki Nakamura +version_added: "1.3" +short_description: Manage hostname +requirements: [ hostname ] +description: + - Set system's hostname + - Currently implemented on only Debian, Ubuntu, RedHat and CentOS. +options: + name: + required: true + description: + - Name of the host +''' + +EXAMPLES = ''' +- hostname: name=web01 +''' + +import os +import syslog +import platform + +# select whether we dump additional debug info through syslog +syslogging = False + +def log(msg): + if syslogging: + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.syslog(syslog.LOG_NOTICE, msg) + +class Hostname(object): + """ + This is a generic Hostname manipulation class that is subclassed + based on platform. + + A subclass may wish to set different strategy instance to self.strategy. + + All subclasses MUST define platform and distribution (which may be None). + """ + + platform = 'Generic' + distribution = None + strategy_class = None + + def __new__(cls, *args, **kwargs): + return load_platform_subclass(Hostname, args, kwargs) + + def __init__(self, module): + self.module = module + self.name = module.params['name'] + self.strategy = self.strategy_class(module) + + def get_current_hostname(self): + return self.strategy.get_current_hostname() + + def set_current_hostname(self, name): + self.strategy.set_current_hostname(name) + + def get_permanent_hostname(self): + return self.strategy.get_permanent_hostname() + + def set_permanent_hostname(self, name): + self.strategy.set_permanent_hostname(name) + +class GenericStrategy(object): + """ + This is a generic Hostname manipulation strategy class. + + A subclass may wish to override some or all of these methods. + - get_current_hostname() + - get_permanent_hostname() + - set_current_hostname(name) + - set_permanent_hostname(name) + """ + def __init__(self, module): + self.module = module + + def execute_command(self, cmd): + if syslogging: + log('Command %s' % '|'.join(cmd)) + + return self.module.run_command(cmd) + + HOSTNAME_CMD = '/bin/hostname' + + def get_current_hostname(self): + cmd = [self.HOSTNAME_CMD] + rc, out, err = self.execute_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + return out.strip() + + def set_current_hostname(self, name): + cmd = [self.HOSTNAME_CMD, name] + rc, out, err = self.execute_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + + def get_permanent_hostname(self): + return None + + def set_permanent_hostname(self, name): + pass + +# =========================================== + +class DebianStrategy(GenericStrategy): + """ + This is a Debian family Hostname manipulation strategy class - it edits + the /etc/hostname file. + """ + + HOSTNAME_FILE = '/etc/hostname' + + def get_permanent_hostname(self): + try: + with open(self.HOSTNAME_FILE) as f: + return f.read().split() + except Exception, err: + self.module.fail_json(msg="failed to read hostname: %s" % + str(err)) + + def set_permanent_hostname(self, name): + try: + with open(self.HOSTNAME_FILE, 'w+') as f: + f.write("%s\n" % name) + except Exception, err: + self.module.fail_json(msg="failed to update hostname: %s" % + str(err)) + +class DebianHostname(Hostname): + platform = 'Linux' + distribution = 'Debian' + strategy_class = DebianStrategy + +class UbuntuHostname(Hostname): + platform = 'Linux' + distribution = 'Ubuntu' + strategy_class = DebianStrategy + +# =========================================== + +class RedHatStrategy(GenericStrategy): + """ + This is a Redhat Hostname strategy class - it edits the + /etc/sysconfig/network file. + """ + NETWORK_FILE = '/etc/sysconfig/network' + + def get_permanent_hostname(self): + try: + with open(self.NETWORK_FILE, 'rb') as f: + for line in f.readlines(): + if line.startswith('HOSTNAME'): + k, v = line.split('=') + return v.strip() + except Exception, err: + self.module.fail_json(msg="failed to read hostname: %s" % + str(err)) + + def set_permanent_hostname(self, name): + try: + lines = [] + with open(self.NETWORK_FILE, 'rb') as f: + for line in f.readlines(): + if line.startswith('HOSTNAME'): + lines.append("HOSTNAME=%s\n" % name) + else: + lines.append(line) + with open(self.NETWORK_FILE, 'w+') as f: + f.writelines(lines) + except Exception, err: + self.module.fail_json(msg="failed to update hostname: %s" % + str(err)) + +class RedHatHostname(Hostname): + platform = 'Linux' + distribution = 'Redhat' + strategy_class = RedHatStrategy + +class CentOSHostname(Hostname): + platform = 'Linux' + distribution = 'Centos' + strategy_class = RedHatStrategy + +# =========================================== + +def main(): + module = AnsibleModule( + argument_spec = dict( + name=dict(required=True, type='str') + ) + ) + + hostname = Hostname(module) + if syslogging: + log('Hostname instantiated - platform %s' % hostname.platform) + if hostname.distribution: + log('Hostname instantiated - distribution %s' % + hostname.distribution) + + changed = False + name = module.params['name'] + current_name = hostname.get_current_hostname() + if syslogging: + log('current_hostname=%s' % current_name) + if current_name != name: + hostname.set_current_hostname(name) + changed = True + + permanent_name = hostname.get_permanent_hostname() + if syslogging: + log('permanent_hostname=%s' % permanent_name) + if permanent_name != name: + hostname.set_permanent_hostname(name) + changed = True + + module.exit_json(changed=changed, name=name) + +# include magic from lib/ansible/module_common.py +#<> +main() From 14fde81ad5267ead0e337e995c778a9b5346d473 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 25 Aug 2013 07:32:37 +0900 Subject: [PATCH 2/8] Raise an error on unsupported platform/distributions. --- system/hostname | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/system/hostname b/system/hostname index 76991edf63b..00007357abd 100644 --- a/system/hostname +++ b/system/hostname @@ -51,6 +51,30 @@ def log(msg): syslog.openlog('ansible-%s' % os.path.basename(__file__)) syslog.syslog(syslog.LOG_NOTICE, msg) +class UnimplementedStrategy(object): + def __init__(self, module): + self.module = module + + def get_current_hostname(self): + self.unimplemented_error() + + def set_current_hostname(self, name): + self.unimplemented_error() + + def get_permanent_hostname(self): + self.unimplemented_error() + + def set_permanent_hostname(self, name): + self.unimplemented_error() + + def unimplemented_error(self): + platform = get_platform() + distribution = get_distribution() + msg_platform = '%s (%s)' % (platform, distribution) \ + if distribution is not None else platform + self.module.fail_json( + msg='hostname module cannot be used on platform %s' % msg_platform) + class Hostname(object): """ This is a generic Hostname manipulation class that is subclassed @@ -63,7 +87,7 @@ class Hostname(object): platform = 'Generic' distribution = None - strategy_class = None + strategy_class = UnimplementedStrategy def __new__(cls, *args, **kwargs): return load_platform_subclass(Hostname, args, kwargs) From 860cf75fdcdd51882979a484b65246e46aa5fad8 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 25 Aug 2013 07:35:20 +0900 Subject: [PATCH 3/8] Remove extra sysloggings. --- system/hostname | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/system/hostname b/system/hostname index 00007357abd..efaff11a487 100644 --- a/system/hostname +++ b/system/hostname @@ -39,18 +39,6 @@ EXAMPLES = ''' - hostname: name=web01 ''' -import os -import syslog -import platform - -# select whether we dump additional debug info through syslog -syslogging = False - -def log(msg): - if syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, msg) - class UnimplementedStrategy(object): def __init__(self, module): self.module = module @@ -122,17 +110,11 @@ class GenericStrategy(object): def __init__(self, module): self.module = module - def execute_command(self, cmd): - if syslogging: - log('Command %s' % '|'.join(cmd)) - - return self.module.run_command(cmd) - HOSTNAME_CMD = '/bin/hostname' def get_current_hostname(self): cmd = [self.HOSTNAME_CMD] - rc, out, err = self.execute_command(cmd) + rc, out, err = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % (rc, out, err)) @@ -140,7 +122,7 @@ class GenericStrategy(object): def set_current_hostname(self, name): cmd = [self.HOSTNAME_CMD, name] - rc, out, err = self.execute_command(cmd) + rc, out, err = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % (rc, out, err)) @@ -242,24 +224,15 @@ def main(): ) hostname = Hostname(module) - if syslogging: - log('Hostname instantiated - platform %s' % hostname.platform) - if hostname.distribution: - log('Hostname instantiated - distribution %s' % - hostname.distribution) changed = False name = module.params['name'] current_name = hostname.get_current_hostname() - if syslogging: - log('current_hostname=%s' % current_name) if current_name != name: hostname.set_current_hostname(name) changed = True permanent_name = hostname.get_permanent_hostname() - if syslogging: - log('permanent_hostname=%s' % permanent_name) if permanent_name != name: hostname.set_permanent_hostname(name) changed = True From bf21ba1521cf87dd6766f36e06c21b1b6c739b88 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 25 Aug 2013 07:44:18 +0900 Subject: [PATCH 4/8] Rewrite with "try ... finally" instead of "with" statement to support Python 2.4. --- system/hostname | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/system/hostname b/system/hostname index efaff11a487..17cdda89415 100644 --- a/system/hostname +++ b/system/hostname @@ -145,16 +145,22 @@ class DebianStrategy(GenericStrategy): def get_permanent_hostname(self): try: - with open(self.HOSTNAME_FILE) as f: + f = open(self.HOSTNAME_FILE) + try: return f.read().split() + finally: + f.close() except Exception, err: self.module.fail_json(msg="failed to read hostname: %s" % str(err)) def set_permanent_hostname(self, name): try: - with open(self.HOSTNAME_FILE, 'w+') as f: + f = open(self.HOSTNAME_FILE, 'w+') + try: f.write("%s\n" % name) + finally: + f.close() except Exception, err: self.module.fail_json(msg="failed to update hostname: %s" % str(err)) @@ -180,11 +186,14 @@ class RedHatStrategy(GenericStrategy): def get_permanent_hostname(self): try: - with open(self.NETWORK_FILE, 'rb') as f: + f = open(self.NETWORK_FILE, 'rb') + try: for line in f.readlines(): if line.startswith('HOSTNAME'): k, v = line.split('=') return v.strip() + finally: + f.close() except Exception, err: self.module.fail_json(msg="failed to read hostname: %s" % str(err)) @@ -192,14 +201,20 @@ class RedHatStrategy(GenericStrategy): def set_permanent_hostname(self, name): try: lines = [] - with open(self.NETWORK_FILE, 'rb') as f: + f = open(self.NETWORK_FILE, 'rb') + try: for line in f.readlines(): if line.startswith('HOSTNAME'): lines.append("HOSTNAME=%s\n" % name) else: lines.append(line) - with open(self.NETWORK_FILE, 'w+') as f: + finally: + f.close() + f = open(self.NETWORK_FILE, 'w+') + try: f.writelines(lines) + finally: + f.close() except Exception, err: self.module.fail_json(msg="failed to update hostname: %s" % str(err)) From 2bef08705f54d8256e5bde6b8f4887d54846135f Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 25 Aug 2013 07:46:23 +0900 Subject: [PATCH 5/8] Fix a bug in DebianStrategy.get_permanent_hostname(). Use strip, not split! --- system/hostname | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hostname b/system/hostname index 17cdda89415..9836b98c71f 100644 --- a/system/hostname +++ b/system/hostname @@ -147,7 +147,7 @@ class DebianStrategy(GenericStrategy): try: f = open(self.HOSTNAME_FILE) try: - return f.read().split() + return f.read().strip() finally: f.close() except Exception, err: From 7bd59c4b23e9fb5131754280584fddc560088299 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 15 Oct 2013 23:41:49 +0900 Subject: [PATCH 6/8] Do not use shortcut conditional operator which is not supported in Python 2.4. --- system/hostname | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/hostname b/system/hostname index 9836b98c71f..826e0f630d2 100644 --- a/system/hostname +++ b/system/hostname @@ -58,8 +58,10 @@ class UnimplementedStrategy(object): def unimplemented_error(self): platform = get_platform() distribution = get_distribution() - msg_platform = '%s (%s)' % (platform, distribution) \ - if distribution is not None else platform + if distribution is not None: + msg_platform = '%s (%s)' % (platform, distribution) + else: + msg_platform = platform self.module.fail_json( msg='hostname module cannot be used on platform %s' % msg_platform) From 744b758633f338dea5d88dbed4ffff8de0d4a5a9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 16 Oct 2013 01:15:47 +0900 Subject: [PATCH 7/8] Add support for Fedora, OpenSUSE and ArchLinux. Tested under Fedora 19 and OpenSUSE 12.3. Not tested on ArchLinux. --- system/hostname | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/system/hostname b/system/hostname index 826e0f630d2..db814aad774 100644 --- a/system/hostname +++ b/system/hostname @@ -233,6 +233,64 @@ class CentOSHostname(Hostname): # =========================================== +class FedoraStrategy(GenericStrategy): + """ + This is a Fedora family Hostname manipulation strategy class - it uses + the hostnamectl command. + """ + + def get_current_hostname(self): + cmd = ['hostname'] + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + return out.strip() + + def set_current_hostname(self, name): + cmd = ['hostnamectl', '--transient', 'set-hostname', name] + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + + def get_permanent_hostname(self): + cmd = 'hostnamectl status | awk \'/^ *Static hostname:/{printf("%s", $3)}\'' + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + return out + + def set_permanent_hostname(self, name): + cmd = ['hostnamectl', '--pretty', 'set-hostname', name] + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + cmd = ['hostnamectl', '--static', 'set-hostname', name] + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + +class FedoraHostname(Hostname): + platform = 'Linux' + distribution = 'Fedora' + strategy_class = FedoraStrategy + +class OpenSUSEHostname(Hostname): + platform = 'Linux' + distribution = 'Opensuse ' + strategy_class = FedoraStrategy + +class ArchHostname(Hostname): + platform = 'Linux' + distribution = 'Arch' + strategy_class = FedoraStrategy + +# =========================================== + def main(): module = AnsibleModule( argument_spec = dict( From 7aa728a32f76d34f483c92ab7a85db8184871374 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Nov 2013 23:32:14 +0900 Subject: [PATCH 8/8] Fix for RHEL6. --- system/hostname | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hostname b/system/hostname index db814aad774..cda955716a1 100644 --- a/system/hostname +++ b/system/hostname @@ -223,7 +223,7 @@ class RedHatStrategy(GenericStrategy): class RedHatHostname(Hostname): platform = 'Linux' - distribution = 'Redhat' + distribution = 'Red hat enterprise linux server' strategy_class = RedHatStrategy class CentOSHostname(Hostname):