merge branch 'th/wait-nm'

https://github.com/linux-system-roles/network/pull/4
This commit is contained in:
Thomas Haller 2017-05-30 14:17:10 +02:00
commit abb8945079
2 changed files with 192 additions and 47 deletions

View file

@ -85,10 +85,11 @@ class Util:
if n is None:
import gi
gi.require_version('NM', '1.0')
from gi.repository import NM, GLib, Gio
from gi.repository import NM, GLib, Gio, GObject
cls._NM = NM
cls._GLib = GLib
cls._Gio = Gio
cls._GObject = GObject
n = NM
import uuid
cls._uuid = uuid
@ -104,6 +105,11 @@ class Util:
cls.NM()
return cls._Gio
@classmethod
def GObject(cls):
cls.NM()
return cls._GObject
@classmethod
def Timestamp(cls):
return cls.GLib().get_monotonic_time()
@ -447,18 +453,25 @@ class ArgValidatorStr(ArgValidator):
raise ValidationError(name, 'cannot be empty')
return v
class ArgValidatorInt(ArgValidator):
def __init__(self, name, required = False, val_min = None, val_max = None, default_value = 0):
ArgValidator.__init__(self, name, required, default_value)
class ArgValidatorNum(ArgValidator):
def __init__(self, name, required = False, val_min = None, val_max = None,
default_value = ArgValidator.MISSING,
numeric_type = int):
ArgValidator.__init__(self, name, required, \
numeric_type(0) if default_value is ArgValidator.MISSING else default_value)
self.val_min = val_min
self.val_max = val_max
self.numeric_type = numeric_type
def _validate(self, value, name):
v = None
try:
if isinstance(value, int):
if isinstance(value, self.numeric_type):
v = value
if isinstance(value, Util.STRING_TYPE):
v = int(value)
else:
v2 = self.numeric_type(value)
if isinstance(value, Util.STRING_TYPE) \
or v2 == value:
v = v2
except:
pass
if v is None:
@ -584,10 +597,10 @@ class ArgValidator_DictIP(ArgValidatorDict):
ArgValidatorBool('dhcp4', default_value = None),
ArgValidatorBool('dhcp4_send_hostname', default_value = None),
ArgValidatorIP ('gateway4', family = socket.AF_INET),
ArgValidatorInt ('route_metric4', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorNum ('route_metric4', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorBool('auto6', default_value = None),
ArgValidatorIP ('gateway6', family = socket.AF_INET6),
ArgValidatorInt ('route_metric6', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorNum ('route_metric6', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorList('address',
nested = ArgValidatorIPAddr('address[?]'),
default_value = list,
@ -642,7 +655,7 @@ class ArgValidator_DictConnection(ArgValidatorDict):
nested = [
ArgValidatorStr ('name'),
ArgValidatorStr ('state', enum_values = ArgValidator_DictConnection.VALID_STATES),
ArgValidatorInt ('wait', val_min = -1, val_max = 1200),
ArgValidatorNum ('wait', val_min = 0, val_max = 3600, numeric_type = float),
ArgValidatorStr ('type', enum_values = ArgValidator_DictConnection.VALID_TYPES),
ArgValidatorBool('autoconnect', default_value = True),
ArgValidatorStr ('slave_type', enum_values = ArgValidator_DictConnection.VALID_SLAVE_TYPES),
@ -651,7 +664,7 @@ class ArgValidator_DictConnection(ArgValidatorDict):
ArgValidatorMac ('mac'),
ArgValidatorBool('check_iface_exists', default_value = True),
ArgValidatorStr ('parent'),
ArgValidatorInt ('vlan_id', val_min = 0, val_max = 4095, default_value = None),
ArgValidatorNum ('vlan_id', val_min = 0, val_max = 4095, default_value = None),
ArgValidatorBool('ignore_errors', default_value = None),
ArgValidator_DictIP(),
],
@ -694,14 +707,9 @@ class ArgValidator_DictConnection(ArgValidatorDict):
if 'name' not in result:
raise ValidationError(name, 'missing "name"')
if result['state'] == 'wait':
if result.get('wait', -1) == -1:
result['wait'] = 10
elif result['wait'] == 0:
raise ValidationError(name + '.wait', 'the "wait" value for state "wait" must be positive')
elif result['state'] in ['up', 'down']:
if result.get('wait', -1) == -1:
result['wait'] = 90
if result['state'] in [ 'wait', 'up', 'down' ]:
if 'wait' not in result:
result['wait'] = None
else:
if 'wait' in result:
raise ValidationError(name + '.wait', '"wait" is not allowed for state "%s"' % (result['state']))
@ -1095,6 +1103,16 @@ class NMUtil:
nmclient = Util.NM().Client.new(None)
self.nmclient = nmclient
def device_is_master_type(self, dev):
if dev:
NM = Util.NM()
GObject = Util.GObject()
if GObject.type_is_a(dev, NM.DeviceBond) \
or GObject.type_is_a(dev, NM.DeviceBridge) \
or GObject.type_is_a(dev, NM.DeviceTeam):
return True
return False
def active_connection_list(self, connections = None, black_list = None, mainloop_iterate = True):
if mainloop_iterate:
Util.GMainLoop_iterate_all()
@ -1268,7 +1286,7 @@ class NMUtil:
raise MyError('created connection failed to normalize: %s' % (e))
return con
def connection_add(self, con, timeout = 5):
def connection_add(self, con, timeout = 10):
def add_cb(client, result, cb_args):
con = None
@ -1291,9 +1309,7 @@ class NMUtil:
raise MyError('failure to add connection: %s' % (cb_args.get('error', 'unknown error')))
return cb_args['con']
def connection_update(self, con, con_new, timeout = 5):
NM = Util.NM()
def connection_update(self, con, con_new, timeout = 10):
con.replace_settings_from_connection(con_new)
def update_cb(connection, result, cb_args):
@ -1317,7 +1333,7 @@ class NMUtil:
raise MyError('failure to update connection: %s' % (cb_args.get('error', 'unknown error')))
return True
def connection_delete(self, connection, timeout = 5):
def connection_delete(self, connection, timeout = 10):
def delete_cb(connection, result, cb_args):
success = False
@ -1339,7 +1355,7 @@ class NMUtil:
if not cb_args.get('success', False):
raise MyError('failure to delete connection: %s' % (cb_args.get('error', 'unknown error')))
def connection_activate(self, connection, timeout = 5):
def connection_activate(self, connection, timeout = 15, wait_time = None):
def activate_cb(client, result, cb_args):
active_connection = None
@ -1360,9 +1376,85 @@ class NMUtil:
raise MyError('failure to activate connection: %s' % ('timeout'))
if not cb_args.get('active_connection', None):
raise MyError('failure to activate connection: %s' % (cb_args.get('error', 'unknown error')))
return cb_args['active_connection']
def active_connection_deactivate(self, ac, timeout = 5):
ac = cb_args['active_connection']
self.connection_activate_wait(ac, wait_time)
return ac
def connection_activate_wait(self, ac, wait_time):
if not wait_time:
return
NM = Util.NM()
state = ac.get_state()
if state == NM.ActiveConnectionState.ACTIVATED:
return
if state != NM.ActiveConnectionState.ACTIVATING:
raise MyError('activation is in unexpected state "%s"' % (state))
def check_activated(ac, dev):
ac_state = ac.get_state()
ac_reason = ac.get_state_reason()
if dev:
dev_state = dev.get_state()
if ac_state == NM.ActiveConnectionState.ACTIVATING:
if self.device_is_master_type(dev) \
and dev_state >= NM.DeviceState.IP_CONFIG \
and dev_state <= NM.DeviceState.ACTIVATED:
# master connections qualify as activated once they reach IP-Config state.
# That is because they may wait for slave devices to attach
return True, None
# fall through
elif ac_state == NM.ActiveConnectionState.ACTIVATED:
return True, None
elif ac_state == NM.ActiveConnectionState.DEACTIVATED:
if not dev \
or ac_reason != NM.ActiveConnectionStateReason.DEVICE_DISCONNECTED \
or dev.get_active_connection() is not ac:
return True, (ac_reason.value_nick or 'unknown reason')
# the state of the active connection is not very helpful.
# see if the device-state is better.
if dev_state <= NM.DeviceState.DISCONNECTED or dev_state > NM.DeviceState.DEACTIVATING:
return True, (dev.get_state_reason().value_nick or ac_reason.value_nick or 'unknown reason')
# fall through, wait longer for a better state reason.
# wait longer.
return False, None
dev = Util.first(ac.get_devices())
complete, failure_reason = check_activated(ac, dev)
if not complete:
cb_out = []
def check_activated_cb():
complete, failure_reason = check_activated(ac, dev)
if complete:
cb_out.append(failure_reason)
Util.GMainLoop().quit()
ac_id = ac.connect('state-changed', lambda source, state, reason: check_activated_cb())
if dev:
dev_id = dev.connect('notify::state', lambda source, pspec: check_activated_cb())
try:
if not Util.GMainLoop_run(wait_time):
raise MyError('connection not fully activated after timeout')
finally:
if dev:
dev.handler_disconnect(dev_id)
ac.handler_disconnect(ac_id)
failure_reason = cb_out[0]
if failure_reason:
raise MyError('connection not activated: %s' % (failure_reason))
def active_connection_deactivate(self, ac, timeout = 10, wait_time = None):
def deactivate_cb(client, result, cb_args):
success = False
@ -1383,8 +1475,35 @@ class NMUtil:
raise MyError('failure to deactivate connection: %s' % (timeout))
if not cb_args.get('success', False):
raise MyError('failure to deactivate connection: %s' % (cb_args.get('error', 'unknown error')))
self.active_connection_deactivate_wait(ac, wait_time)
return True
def active_connection_deactivate_wait(self, ac, wait_time):
if not wait_time:
return
NM = Util.NM()
def check_deactivated(ac):
return ac.get_state() >= NM.ActiveConnectionState.DEACTIVATED
if not check_deactivated(ac):
def check_deactivated_cb():
if check_deactivated(ac):
Util.GMainLoop().quit()
ac_id = ac.connect('notify::state', lambda source, pspec: check_deactivated_cb())
try:
if not Util.GMainLoop_run(wait_time):
raise MyError('connection not fully deactivated after timeout')
finally:
ac.handler_disconnect(ac_id)
###############################################################################
class _AnsibleUtil:
@ -1571,10 +1690,13 @@ class Cmd:
try:
state = connection['state']
if state == 'wait':
AnsibleUtil.log_info(idx, 'wait for %s seconds' % (connection['wait']))
w = connection['wait']
if w is None:
w = 10
AnsibleUtil.log_info(idx, 'wait for %s seconds' % (w))
if AnsibleUtil.check_mode == CheckMode.REAL_RUN:
import time
time.sleep(connection['wait'])
time.sleep(w)
elif state == 'absent':
self.run_state_absent(idx)
elif state == 'present':
@ -1729,9 +1851,6 @@ class Cmd_nm(Cmd):
def run_state_up(self, idx):
connection = AnsibleUtil.connections[idx]
if connection['wait'] != 0:
AnsibleUtil.log_warn(idx, 'wait for activation is not yet implemented')
con = Util.first(self.nmutil.connection_list(name = connection['name'], uuid = connection['nm.uuid']))
if not con:
if AnsibleUtil.check_mode == CheckMode.REAL_RUN:
@ -1742,18 +1861,24 @@ class Cmd_nm(Cmd):
AnsibleUtil.log_info(idx, 'up connection %s, %s' % (con.get_id(), con.get_uuid()))
if AnsibleUtil.check_mode == CheckMode.REAL_RUN:
try:
self.nmutil.connection_activate (con)
ac = self.nmutil.connection_activate (con)
except MyError as e:
AnsibleUtil.log_error(idx, 'up connection failed: %s' % (e))
wait_time = connection['wait']
if wait_time is None:
wait_time = 90
try:
self.nmutil.connection_activate_wait(ac, wait_time)
except MyError as e:
AnsibleUtil.log_error(idx, 'up connection failed while waiting: %s' % (e))
AnsibleUtil.run_results_changed(idx)
def run_state_down(self, idx):
connection = AnsibleUtil.connections[idx]
if connection['wait'] != 0:
AnsibleUtil.log_warn(idx, 'wait for activation is not yet implemented')
cons = self.nmutil.connection_list(name = connection['name'])
changed = False
if cons:
@ -1770,6 +1895,16 @@ class Cmd_nm(Cmd):
self.nmutil.active_connection_deactivate(ac)
except MyError as e:
AnsibleUtil.log_error(idx, 'down connection failed: %s' % (e))
wait_time = connection['wait']
if wait_time is None:
wait_time = 10
try:
self.nmutil.active_connection_deactivate_wait(ac, wait_time)
except MyError as e:
AnsibleUtil.log_error(idx, 'down connection failed while waiting: %s' % (e))
cons = self.nmutil.connection_list(name = connection['name'])
if not changed:
@ -1863,7 +1998,7 @@ class Cmd_initscripts(Cmd):
connection = AnsibleUtil.connections[idx]
name = connection['name']
if connection['wait'] != 0:
if connection['wait'] is not None:
# initscripts don't support wait, they always block until the ifup/ifdown
# command completes. Silently ignore the argument.
pass

View file

@ -27,13 +27,23 @@ class TestValidator(unittest.TestCase):
def test_validate_int(self):
v = n.ArgValidatorInt('state', default_value = None)
v = n.ArgValidatorNum('state', default_value = None, numeric_type = float)
self.assertEqual(1, v.validate(1))
self.assertEqual(1, v.validate("1"))
self.assertEqual(1.5, v.validate(1.5))
self.assertEqual(1.5, v.validate("1.5"))
self.assertValidationError(v, None)
self.assertValidationError(v, "1a")
v = n.ArgValidatorInt('state', required = True)
v = n.ArgValidatorNum('state', default_value = None)
self.assertEqual(1, v.validate(1))
self.assertEqual(1, v.validate(1.0))
self.assertEqual(1, v.validate("1"))
self.assertValidationError(v, None)
self.assertValidationError(v, None)
self.assertValidationError(v, 1.5)
self.assertValidationError(v, "1.5")
v = n.ArgValidatorNum('state', required = True)
self.assertValidationError(v, None)
def test_validate_bool(self):
@ -56,7 +66,7 @@ class TestValidator(unittest.TestCase):
v = n.ArgValidatorDict(
'dict',
nested = [
n.ArgValidatorInt('i', required = True),
n.ArgValidatorNum('i', required = True),
n.ArgValidatorStr('s', required = False, default_value = 's_default'),
n.ArgValidatorStr('l', required = False, default_value = n.ArgValidator.MISSING),
])
@ -87,7 +97,7 @@ class TestValidator(unittest.TestCase):
v = n.ArgValidatorList(
'list',
nested = n.ArgValidatorInt('i')
nested = n.ArgValidatorNum('i')
)
self.assertEqual(
[ 1, 5 ],
@ -134,7 +144,7 @@ class TestValidator(unittest.TestCase):
{
'name': '5',
'state': 'up',
'wait': 90,
'wait': None,
'ignore_errors': None,
}
],
@ -173,7 +183,7 @@ class TestValidator(unittest.TestCase):
'interface_name': None,
'check_iface_exists': True,
'slave_type': None,
'wait': 90,
'wait': None,
},
],
n.AnsibleUtil.ARGS_CONNECTIONS.validate([
@ -275,7 +285,7 @@ class TestValidator(unittest.TestCase):
'interface_name': None,
'check_iface_exists': True,
'slave_type': None,
'wait': 90,
'wait': None,
},
],
n.AnsibleUtil.ARGS_CONNECTIONS.validate([
@ -314,7 +324,7 @@ class TestValidator(unittest.TestCase):
'interface_name': None,
'check_iface_exists': True,
'slave_type': None,
'wait': 90,
'wait': None,
},
],
n.AnsibleUtil.ARGS_CONNECTIONS.validate([
@ -355,7 +365,7 @@ class TestValidator(unittest.TestCase):
'interface_name': None,
'check_iface_exists': True,
'slave_type': None,
'wait': 90,
'wait': None,
},
],
n.AnsibleUtil.ARGS_CONNECTIONS.validate([