Adds NTP configuration based on DHCP
This commit is contained in:
parent
19e6edd39a
commit
73727b8ae9
@ -463,6 +463,32 @@ class WindowsUtils(base.BaseOSUtils):
|
||||
l.append(r.Name)
|
||||
return l
|
||||
|
||||
def get_dhcp_hosts_in_use(self):
|
||||
dhcp_hosts = []
|
||||
conn = wmi.WMI(moniker='//./root/cimv2')
|
||||
for net_cfg in conn.Win32_NetworkAdapterConfiguration(
|
||||
DHCPEnabled=True):
|
||||
if net_cfg.DHCPServer:
|
||||
dhcp_hosts.append(str(net_cfg.DHCPServer))
|
||||
return dhcp_hosts
|
||||
|
||||
def set_ntp_client_config(self, ntp_host):
|
||||
if self.check_sysnative_dir_exists():
|
||||
base_dir = self.get_sysnative_dir()
|
||||
else:
|
||||
base_dir = self.get_system32_dir()
|
||||
|
||||
w32tm_path = os.path.join(base_dir, "w32tm.exe")
|
||||
|
||||
args = [w32tm_path, '/config', '/manualpeerlist:%s' % ntp_host,
|
||||
'/syncfromflags:manual', '/update']
|
||||
|
||||
(out, err, ret_val) = self.execute_process(args, False)
|
||||
if ret_val:
|
||||
raise Exception('w32tm failed to configure NTP.\n'
|
||||
'Output: %(out)s\nError: %(err)s' %
|
||||
{'out': out, 'err': err})
|
||||
|
||||
def set_static_network_config(self, adapter_name, address, netmask,
|
||||
broadcast, gateway, dnsnameservers):
|
||||
conn = wmi.WMI(moniker='//./root/cimv2')
|
||||
|
@ -22,6 +22,7 @@ opts = [
|
||||
cfg.ListOpt(
|
||||
'plugins',
|
||||
default=[
|
||||
'cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin',
|
||||
'cloudbaseinit.plugins.windows.sethostname.SetHostNamePlugin',
|
||||
'cloudbaseinit.plugins.windows.createuser.CreateUserPlugin',
|
||||
'cloudbaseinit.plugins.windows.networkconfig.NetworkConfigPlugin',
|
||||
|
90
cloudbaseinit/plugins/windows/ntpclient.py
Normal file
90
cloudbaseinit/plugins/windows/ntpclient.py
Normal file
@ -0,0 +1,90 @@
|
||||
# Copyright 2014 Cloudbase Solutions Srl
|
||||
#
|
||||
# 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.
|
||||
|
||||
import socket
|
||||
import time
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from cloudbaseinit.openstack.common import log as logging
|
||||
from cloudbaseinit.osutils import factory as osutils_factory
|
||||
from cloudbaseinit.plugins import base
|
||||
from cloudbaseinit.utils import dhcp
|
||||
|
||||
opts = [
|
||||
cfg.BoolOpt('ntp_use_dhcp_config', default=False,
|
||||
help='Configures NTP client time synchronization using '
|
||||
'the NTP servers provided via DHCP'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(opts)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NTPClientPlugin(base.BasePlugin):
|
||||
def _check_w32time_svc_status(self, osutils):
|
||||
_W32TIME_SERVICE = "w32time"
|
||||
|
||||
svc_start_mode = osutils.get_service_start_mode(
|
||||
_W32TIME_SERVICE)
|
||||
|
||||
if svc_start_mode != osutils.SERVICE_START_MODE_AUTOMATIC:
|
||||
osutils.set_service_start_mode(
|
||||
_W32TIME_SERVICE,
|
||||
osutils.SERVICE_START_MODE_AUTOMATIC)
|
||||
|
||||
svc_status = osutils.get_service_status(_W32TIME_SERVICE)
|
||||
if svc_status == osutils.SERVICE_STATUS_STOPPED:
|
||||
osutils.start_service(_W32TIME_SERVICE)
|
||||
|
||||
i = 0
|
||||
max_retries = 30
|
||||
while svc_status != osutils.SERVICE_STATUS_RUNNING:
|
||||
if i >= max_retries:
|
||||
raise Exception('Service %s did not start' %
|
||||
_W32TIME_SERVICE)
|
||||
time.sleep(1)
|
||||
svc_status = osutils.get_service_status(_W32TIME_SERVICE)
|
||||
i += 1
|
||||
|
||||
def execute(self, service, shared_data):
|
||||
if CONF.ntp_use_dhcp_config:
|
||||
osutils = osutils_factory.get_os_utils()
|
||||
dhcp_hosts = osutils.get_dhcp_hosts_in_use()
|
||||
|
||||
ntp_option_data = None
|
||||
|
||||
for dhcp_host in dhcp_hosts:
|
||||
options_data = dhcp.get_dhcp_options(dhcp_host,
|
||||
[dhcp.OPTION_NTP_SERVERS])
|
||||
if options_data:
|
||||
ntp_option_data = options_data.get(dhcp.OPTION_NTP_SERVERS)
|
||||
if ntp_option_data:
|
||||
break
|
||||
|
||||
if not ntp_option_data:
|
||||
LOG.debug("Could not obtain the NTP configuration via DHCP")
|
||||
return (base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False)
|
||||
|
||||
# TODO(alexpilotti): support multiple NTP servers
|
||||
ntp_host = socket.inet_ntoa(ntp_option_data[:4])
|
||||
|
||||
self._check_w32time_svc_status(osutils)
|
||||
osutils.set_ntp_client_config(ntp_host)
|
||||
|
||||
LOG.info('NTP client configured. Server: %s' % ntp_host)
|
||||
|
||||
return (base.PLUGIN_EXECUTION_DONE, False)
|
127
cloudbaseinit/utils/dhcp.py
Normal file
127
cloudbaseinit/utils/dhcp.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright 2014 Cloudbase Solutions Srl
|
||||
#
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
import netifaces
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
|
||||
_DHCP_COOKIE = b'\x63\x82\x53\x63'
|
||||
_OPTION_END = b'\xff'
|
||||
|
||||
OPTION_NTP_SERVERS = 42
|
||||
|
||||
|
||||
def _get_dhcp_request_data(id_req, mac_address_b, requested_options,
|
||||
vendor_id):
|
||||
# See: http://www.ietf.org/rfc/rfc2131.txt
|
||||
data = b'\x01'
|
||||
data += b'\x01'
|
||||
data += b'\x06'
|
||||
data += b'\x00'
|
||||
data += struct.pack('!L', id_req)
|
||||
data += b'\x00\x00'
|
||||
data += b'\x00\x00'
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += mac_address_b
|
||||
data += b'\x00' * 10
|
||||
data += b'\x00' * 64
|
||||
data += b'\x00' * 128
|
||||
data += _DHCP_COOKIE
|
||||
data += b'\x35\x01\x01'
|
||||
|
||||
if vendor_id:
|
||||
vendor_id_b = vendor_id.encode('ascii')
|
||||
data += b'\x3c' + struct.pack('b', len(vendor_id_b)) + vendor_id_b
|
||||
|
||||
data += b'\x3d\x07\x01' + mac_address_b
|
||||
data += b'\x37' + struct.pack('b', len(requested_options))
|
||||
|
||||
for option in requested_options:
|
||||
data += struct.pack('b', option)
|
||||
|
||||
data += _OPTION_END
|
||||
return data
|
||||
|
||||
|
||||
def _parse_dhcp_reply(data, id_req):
|
||||
message_type = struct.unpack('b', data[0])[0]
|
||||
|
||||
if message_type != 2:
|
||||
return (False, {})
|
||||
|
||||
id_reply = struct.unpack('!L', data[4:8])[0]
|
||||
if id_reply != id_req:
|
||||
return (False, {})
|
||||
|
||||
if data[236:240] != _DHCP_COOKIE:
|
||||
return (False, {})
|
||||
|
||||
options = {}
|
||||
|
||||
i = 240
|
||||
while data[i] != _OPTION_END:
|
||||
id_option = struct.unpack('b', data[i])[0]
|
||||
option_data_len = struct.unpack('b', data[i + 1])[0]
|
||||
i += 2
|
||||
options[id_option] = data[i: i + option_data_len]
|
||||
i += option_data_len
|
||||
|
||||
return (True, options)
|
||||
|
||||
|
||||
def _get_mac_address_by_local_ip(ip_addr):
|
||||
for iface in netifaces.interfaces():
|
||||
addrs = netifaces.ifaddresses(iface)
|
||||
for addr in addrs[netifaces.AF_INET]:
|
||||
if addr['addr'] == ip_addr:
|
||||
return addrs[netifaces.AF_LINK][0]['addr']
|
||||
|
||||
|
||||
def get_dhcp_options(dhcp_host, requested_options=[], timeout=5.0,
|
||||
vendor_id='cloudbase-init'):
|
||||
id_req = random.randint(0, 2 ** 32 - 1)
|
||||
options = None
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.bind(('', 68))
|
||||
s.settimeout(timeout)
|
||||
s.connect((dhcp_host, 67))
|
||||
|
||||
local_ip_addr = s.getsockname()[0]
|
||||
mac_address = _get_mac_address_by_local_ip(local_ip_addr)
|
||||
|
||||
data = _get_dhcp_request_data(id_req, mac_address, requested_options,
|
||||
vendor_id)
|
||||
s.send(data)
|
||||
|
||||
start = datetime.datetime.now()
|
||||
now = start
|
||||
replied = False
|
||||
while (not replied and
|
||||
now - start < datetime.timedelta(seconds=timeout)):
|
||||
data = s.recv(1024)
|
||||
(replied, options) = _parse_dhcp_reply(data, id_req)
|
||||
now = datetime.datetime.now()
|
||||
except socket.timeout:
|
||||
pass
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
return options
|
@ -9,3 +9,4 @@ oslo.config
|
||||
six>=1.4.1
|
||||
Babel>=1.3
|
||||
oauth
|
||||
netifaces
|
||||
|
Loading…
x
Reference in New Issue
Block a user