lib/chef/resource/hostname.rb



#
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# 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.
#

require_relative "../resource"
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class Resource
    # Sets the hostname and updates /etc/hosts on *nix systems
    # @since 14.0.0
    class Hostname < Chef::Resource

      provides :hostname

      description "Use the **hostname** resource to set the system's hostname, configure hostname and hosts config file, and re-run the Ohai hostname plugin so the hostname will be available in subsequent cookbooks."
      introduced "14.0"
      examples <<~DOC
        **Set the hostname using the IP address, as detected by Ohai**:

        ```ruby
        hostname 'example'
        ```

        **Manually specify the hostname and IP address**:

        ```ruby
        hostname 'statically_configured_host' do
          hostname 'example'
          ipaddress '198.51.100.2'
        end
        ```

        **Change the hostname of a Windows, Non-Domain joined node**:

        ```ruby
        hostname 'renaming a workgroup computer' do
          hostname 'Foo'
        end
        ```

        **Change the hostname of a Windows, Domain-joined node (new in 17.2)**:

        ```ruby
        hostname 'renaming a domain-joined computer' do
          hostname 'Foo'
          domain_user "Domain\\Someone"
          domain_password 'SomePassword'
        end
        ```
      DOC

      property :hostname, String,
        description: "An optional property to set the hostname if it differs from the resource block's name.",
        name_property: true

      property :fqdn, String,
        description: "An optional property to set the fqdn if it differs from the resource block's hostname.",
        introduced: "17.0"

      property :ipaddress, String,
        description: "The IP address to use when configuring the hosts file.",
        default: lazy { node["ipaddress"] }, default_description: "The node's IP address as determined by Ohai."

      property :aliases, [ Array, nil ],
        description: "An array of hostname aliases to use when configuring the hosts file.",
        default: nil

      # override compile_time property to be true by default
      property :compile_time, [ TrueClass, FalseClass ],
        description: "Determines whether or not the resource should be run at compile time.",
        default: true, desired_state: false

      property :windows_reboot, [ TrueClass, FalseClass ],
        description: "Determines whether or not Windows should be reboot after changing the hostname, as this is required for the change to take effect.",
        default: true

      property :domain_user, String,
        description: "A domain account specified in the form of DOMAIN\\user used when renaming a domain-joined device",
        introduced: "17.2"

      property :domain_password, String,
        description: "The password to accompany the domain_user parameter",
        sensitive: true,
        introduced: "17.2"

      action_class do
        def append_replacing_matching_lines(path, regex, string)
          text = IO.read(path).split("\n")
          text.reject! { |s| s =~ regex }
          text += [ string ]
          file path do
            content text.join("\n") + "\n"
            owner "root"
            group node["root_group"]
            mode "0644"
            not_if { IO.read(path).split("\n").include?(string) }
          end
        end

        # read in the xml file used by Ec2ConfigService and update the Ec2SetComputerName
        # setting to disable updating the computer name so we don't revert our change on reboot
        # @return [String]
        def updated_ec2_config_xml
          begin
            require "rexml/document" unless defined?(REXML::Document)
            config = REXML::Document.new(::File.read(WINDOWS_EC2_CONFIG))
            # find an element named State with a sibling element whose value is Ec2SetComputerName
            REXML::XPath.each(config, "//Plugin/State[../Name/text() = 'Ec2SetComputerName']") do |element|
              element.text = "Disabled"
            end
          rescue
            return ""
          end
          config.to_s
        end
      end

      def is_domain_joined?
        powershell_exec!("(Get-CIMInstance -Class Win32_ComputerSystem).PartofDomain").result
      end

      action :set, description: "Sets the node's hostname." do
        if !windows?
          ohai "reload hostname" do
            plugin "hostname"
            action :nothing
          end

          # set the hostname via /bin/hostname
          execute "set hostname to #{new_resource.hostname}" do
            command "/bin/hostname #{new_resource.hostname}"
            not_if { shell_out!("hostname").stdout.chomp == new_resource.hostname }
            notifies :reload, "ohai[reload hostname]"
          end

          # make sure node['fqdn'] resolves via /etc/hosts
          unless new_resource.ipaddress.nil?
            newline = "#{new_resource.ipaddress}"
            newline << " #{new_resource.fqdn}" unless new_resource.fqdn.to_s.empty?
            newline << " #{new_resource.hostname}"
            newline << " #{new_resource.aliases.join(" ")}" if new_resource.aliases && !new_resource.aliases.empty?
            newline << " #{new_resource.hostname[/[^\.]*/]}"
            r = append_replacing_matching_lines("/etc/hosts", /^#{new_resource.ipaddress}\s+|\s+#{new_resource.hostname}\s+/, newline)
            r.atomic_update false if docker?
            r.notifies :reload, "ohai[reload hostname]"
          end

          # setup the hostname to persist on a reboot
          case
          when darwin?
            # darwin
            execute "set HostName via scutil" do
              command "/usr/sbin/scutil --set HostName #{new_resource.hostname}"
              not_if { shell_out("/usr/sbin/scutil --get HostName").stdout.chomp == new_resource.hostname }
              notifies :reload, "ohai[reload hostname]"
            end
            execute "set ComputerName via scutil" do
              command "/usr/sbin/scutil --set ComputerName  #{new_resource.hostname}"
              not_if { shell_out("/usr/sbin/scutil --get ComputerName").stdout.chomp == new_resource.hostname }
              notifies :reload, "ohai[reload hostname]"
            end
            shortname = new_resource.hostname[/[^\.]*/]
            execute "set LocalHostName via scutil" do
              command "/usr/sbin/scutil --set LocalHostName #{shortname}"
              not_if { shell_out("/usr/sbin/scutil --get LocalHostName").stdout.chomp == shortname }
              notifies :reload, "ohai[reload hostname]"
            end
          when linux?
            case
            when ::File.exist?("/usr/bin/hostnamectl") && !docker?
              # use hostnamectl whenever we find it on linux (as systemd takes over the world)
              # this must come before other methods like /etc/hostname and /etc/sysconfig/network
              execute "hostnamectl set-hostname #{new_resource.hostname}" do
                notifies :reload, "ohai[reload hostname]"
                not_if { shell_out!("hostnamectl status", returns: [0, 1]).stdout =~ /Static hostname:\s*#{new_resource.hostname}\s*$/ }
              end
            when ::File.exist?("/etc/hostname")
              # debian family uses /etc/hostname
              # arch also uses /etc/hostname
              # the "platform: iox_xr, platform_family: wrlinux, os: linux" platform also hits this
              # the "platform: nexus, platform_family: wrlinux, os: linux" platform also hits this
              # this is also fallback for any linux systemd host in a docker container (where /usr/bin/hostnamectl will fail)
              file "/etc/hostname" do
                atomic_update false if docker?
                content "#{new_resource.hostname}\n"
                owner "root"
                group node["root_group"]
                mode "0644"
              end
            when ::File.file?("/etc/sysconfig/network")
              # older non-systemd RHEL/Fedora derived
              append_replacing_matching_lines("/etc/sysconfig/network", /^HOSTNAME\s*=/, "HOSTNAME=#{new_resource.hostname}")
            when ::File.exist?("/etc/HOSTNAME")
              # SuSE/openSUSE uses /etc/HOSTNAME
              file "/etc/HOSTNAME" do
                content "#{new_resource.hostname}\n"
                owner "root"
                group node["root_group"]
                mode "0644"
              end
            when ::File.exist?("/etc/conf.d/hostname")
              # Gentoo
              file "/etc/conf.d/hostname" do
                content "hostname=\"#{new_resource.hostname}\"\n"
                owner "root"
                group node["root_group"]
                mode "0644"
              end
            else
              # This is a failsafe for all other linux distributions where we set the hostname
              # via /etc/sysctl.conf on reboot.  This may get into a fight with other cookbooks
              # that manage sysctls on linux.
              append_replacing_matching_lines("/etc/sysctl.conf", /^\s+kernel\.hostname\s+=/, "kernel.hostname=#{new_resource.hostname}")
            end
          when ::File.exist?("/etc/rc.conf")
            # *BSD systems with /etc/rc.conf + /etc/myname
            append_replacing_matching_lines("/etc/rc.conf", /^\s+hostname\s+=/, "hostname=#{new_resource.hostname}")

            file "/etc/myname" do
              content "#{new_resource.hostname}\n"
              owner "root"
              group node["root_group"]
              mode "0644"
            end
          when ::File.exist?("/usr/sbin/svccfg") # solaris 5.11
            execute "svccfg -s system/identity:node setprop config/nodename=\'#{new_resource.hostname}\'" do
              notifies :run, "execute[svcadm refresh]", :immediately
              notifies :run, "execute[svcadm restart]", :immediately
              not_if { shell_out!("svccfg -s system/identity:node listprop config/nodename").stdout.chomp =~ %r{config/nodename\s+astring\s+#{new_resource.hostname}} }
            end
            execute "svcadm refresh" do
              command "svcadm refresh system/identity:node"
              action :nothing
            end
            execute "svcadm restart" do
              command "svcadm restart system/identity:node"
              action :nothing
            end
          else
            raise "Do not know how to set hostname on os #{node["os"]}, platform #{node["platform"]},"\
              "platform_version #{node["platform_version"]}, platform_family #{node["platform_family"]}"
          end

        else # windows
          WINDOWS_EC2_CONFIG = 'C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml'.freeze

          raise "Windows hostnames cannot contain a period." if new_resource.hostname.include?(".")

          # suppress EC2 config service from setting our hostname
          if ::File.exist?(WINDOWS_EC2_CONFIG)
            xml_contents = updated_ec2_config_xml
            if xml_contents.empty?
              Chef::Log.warn('Unable to properly parse and update C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml contents. Skipping file update.')
            else
              file WINDOWS_EC2_CONFIG do
                content xml_contents
              end
            end
          end

          unless Socket.gethostbyname(Socket.gethostname).first == new_resource.hostname
            if is_domain_joined?
              if new_resource.domain_user.nil? || new_resource.domain_password.nil?
                raise "The `domain_user` and `domain_password` properties are required to change the hostname of a domain-connected Windows system."
              else
                converge_by "set hostname to #{new_resource.hostname}" do
                  powershell_exec! <<~EOH
                    $user = #{new_resource.domain_user}
                    $secure_password = #{new_resource.domain_password} | Convertto-SecureString -AsPlainText -Force
                    $Credentials = New-Object System.Management.Automation.PSCredential -Argumentlist ($user, $secure_password)
                    Rename-Computer -NewName #{new_resource.hostname} -DomainCredential $Credentials
                  EOH
                end
              end
            else
              converge_by "set hostname to #{new_resource.hostname}" do
                powershell_exec!("Rename-Computer -NewName #{new_resource.hostname}")
              end
            end
            # reboot because $windows
            reboot "setting hostname" do
              reason "#{ChefUtils::Dist::Infra::PRODUCT} updated system hostname"
              only_if { new_resource.windows_reboot }
              action :request_reboot
            end
          end
        end
      end
    end
  end
end