[lc_ldap] On met en place un système de propagation de certaines modifications.
* Quand on modifie un attribut qui devrait en modifier d'autres, on peut invoquer check_changes pour voir ce qui devrait être changé, puis validate_changes pour rendre ces changements effectifs. Il ne reste plus qu'à appeler save pour enregistrer le tout. * Correction de petits problèmes sur les locks : quand save réussissait, ils n'étaient pas virés.
This commit is contained in:
parent
9540bc572c
commit
c2968c6b15
5 changed files with 133 additions and 28 deletions
12
attributs.py
12
attributs.py
|
@ -141,7 +141,7 @@ class AttrsDict(dict):
|
||||||
def __getitem__(self, attr):
|
def __getitem__(self, attr):
|
||||||
values = super(AttrsDict, self).__getitem__(attr)
|
values = super(AttrsDict, self).__getitem__(attr)
|
||||||
if not isinstance(values, list):
|
if not isinstance(values, list):
|
||||||
values = [ values ]
|
values = [values]
|
||||||
output = []
|
output = []
|
||||||
for val in values:
|
for val in values:
|
||||||
output.append(attrify(val, attr, self._conn, self._parent))
|
output.append(attrify(val, attr, self._conn, self._parent))
|
||||||
|
@ -699,11 +699,9 @@ class ip6HostNumber(Attr):
|
||||||
ldap_name = "ip6HostNumber"
|
ldap_name = "ip6HostNumber"
|
||||||
|
|
||||||
def parse_value(self, ip6):
|
def parse_value(self, ip6):
|
||||||
if self.parent != None:
|
if ip6 == '<automatique>':
|
||||||
ip = ip6_of_mac(str(self.parent['macAddress'][0]), int(str(self.parent['rid'][0])))
|
ip6 = ip6_of_mac(str(self.parent['macAddress'][0]), int(str(self.parent['rid'][0])))
|
||||||
else:
|
self.value = netaddr.IPAddress(ip6)
|
||||||
ip = ip6
|
|
||||||
self.value = netaddr.ip.IPAddress(ip)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.value)
|
return unicode(self.value)
|
||||||
|
@ -726,7 +724,7 @@ class mid(Attr):
|
||||||
@crans_attribute
|
@crans_attribute
|
||||||
class rid(Attr):
|
class rid(Attr):
|
||||||
singlevalue = True
|
singlevalue = True
|
||||||
optional = False
|
optional = True
|
||||||
unique = True
|
unique = True
|
||||||
legend = u"Identifiant réseau de machine"
|
legend = u"Identifiant réseau de machine"
|
||||||
category = 'id'
|
category = 'id'
|
||||||
|
|
|
@ -70,6 +70,8 @@ def ip4_of_rid(rid):
|
||||||
"""Convertit un rid en son IP associée"""
|
"""Convertit un rid en son IP associée"""
|
||||||
# Au cas où
|
# Au cas où
|
||||||
rid = int(rid)
|
rid = int(rid)
|
||||||
|
if rid == -1:
|
||||||
|
return u""
|
||||||
|
|
||||||
net, plage = find_rid_plage(rid)
|
net, plage = find_rid_plage(rid)
|
||||||
if net == 'Inconnu':
|
if net == 'Inconnu':
|
||||||
|
@ -83,10 +85,13 @@ def ip4_of_rid(rid):
|
||||||
try:
|
try:
|
||||||
return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net][0]).first + rid - plage[0])
|
return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net][0]).first + rid - plage[0])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise EnvironmentError("Les machines v6-only ne peuvent pas avoir d'ipv4 (%s)" % (net))
|
return u""
|
||||||
|
|
||||||
def rid_of_ip4(ipv4):
|
def rid_of_ip4(ipv4):
|
||||||
"""Convertit une ipv4 en rid, si possible"""
|
"""Convertit une ipv4 en rid, si possible"""
|
||||||
|
if ipv4 == "":
|
||||||
|
return -1
|
||||||
|
|
||||||
# Est-ce une machine spéciale ?
|
# Est-ce une machine spéciale ?
|
||||||
for (rid, ip) in config.rid_machines_speciales.iteritems():
|
for (rid, ip) in config.rid_machines_speciales.iteritems():
|
||||||
if str(ipv4) == ip:
|
if str(ipv4) == ip:
|
||||||
|
@ -111,8 +116,8 @@ def prefixev6_of_rid(rid):
|
||||||
if net == 'Inconnu':
|
if net == 'Inconnu':
|
||||||
raise ValueError("Rid dans aucune plage: %d" % rid)
|
raise ValueError("Rid dans aucune plage: %d" % rid)
|
||||||
|
|
||||||
# fil-v6 ou wifi-v6, we don't care
|
# adherents-v6 ou wifi-adh-v6, we don't care
|
||||||
return netaddr.IPAddress(netaddr.IPNetwork(config.prefix['fil-v6'][0]).first + 2**64*rid)
|
return netaddr.IPAddress(netaddr.IPNetwork(config.prefix['adherents-v6'][0]).first + 2**64*rid)
|
||||||
|
|
||||||
def ip6_of_mac(mac, rid):
|
def ip6_of_mac(mac, rid):
|
||||||
"""
|
"""
|
||||||
|
@ -120,6 +125,8 @@ def ip6_of_mac(mac, rid):
|
||||||
"""
|
"""
|
||||||
# Au cas où
|
# Au cas où
|
||||||
rid = int(rid)
|
rid = int(rid)
|
||||||
|
if rid == -1:
|
||||||
|
return u""
|
||||||
|
|
||||||
net, plage = find_rid_plage(rid)
|
net, plage = find_rid_plage(rid)
|
||||||
if net == 'Inconnu':
|
if net == 'Inconnu':
|
||||||
|
@ -132,7 +139,7 @@ def ip6_of_mac(mac, rid):
|
||||||
# hex retourne un str, donc on concatène, suivant la RFC
|
# hex retourne un str, donc on concatène, suivant la RFC
|
||||||
euid64v6 = hex(int(mac[:2], 16)^0b00000010) + mac[2:6] + u'fffe' + mac[6:12]
|
euid64v6 = hex(int(mac[:2], 16)^0b00000010) + mac[2:6] + u'fffe' + mac[6:12]
|
||||||
|
|
||||||
# fil-v6 ou wifi-v6, we don't care
|
# adherents-v6 ou wifi-adh-v6, we don't care
|
||||||
if net != "special":
|
if net != "special":
|
||||||
return netaddr.IPAddress(netaddr.IPNetwork(config.prefix[net][0]).first + int(euid64v6, 16))
|
return netaddr.IPAddress(netaddr.IPNetwork(config.prefix[net][0]).first + int(euid64v6, 16))
|
||||||
else:
|
else:
|
||||||
|
|
23
lc_ldap.py
23
lc_ldap.py
|
@ -225,7 +225,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
|
|
||||||
def newMachine(self, parent, realm, mldif, login=None):
|
def newMachine(self, parent, realm, mldif, login=None):
|
||||||
"""Crée une nouvelle machine: ``realm`` peut être:
|
"""Crée une nouvelle machine: ``realm`` peut être:
|
||||||
fil, fil-v6, wifi, wifi-v6, adm, gratuit, personnel-ens, special
|
fil, adherents-v6, wifi, wifi-adh-v6, adm, gratuit, personnel-ens, special
|
||||||
mldif est un uldif pour la machine
|
mldif est un uldif pour la machine
|
||||||
--Partiellement implémenté"""
|
--Partiellement implémenté"""
|
||||||
# On ne veut pas modifier mldif directement
|
# On ne veut pas modifier mldif directement
|
||||||
|
@ -243,22 +243,20 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
uldif['objectClass'] = [u'borneWifi']
|
uldif['objectClass'] = [u'borneWifi']
|
||||||
assert isinstance(owner, objets.AssociationCrans)
|
assert isinstance(owner, objets.AssociationCrans)
|
||||||
|
|
||||||
elif realm in ["wifi-adh", "wifi-v6"]:
|
elif realm in ["wifi-adh", "wifi-adh-v6"]:
|
||||||
uldif['objectClass'] = [u'machineWifi']
|
uldif['objectClass'] = [u'machineWifi']
|
||||||
assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club)
|
assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club)
|
||||||
|
|
||||||
elif realm in ["adherents", "fil-v6", "personnel-ens"]:
|
elif realm in ["adherents", "adherents-v6", "personnel-ens"]:
|
||||||
uldif['objectClass'] = [u'machineFixe']
|
uldif['objectClass'] = [u'machineFixe']
|
||||||
assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club)
|
assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("Realm inconnu: %r" % realm)
|
raise ValueError("Realm inconnu: %r" % realm)
|
||||||
|
|
||||||
# On récupère la plage des mids
|
|
||||||
plage = itertools.chain(*[xrange(a,b+1) for (a,b) in config.rid_primaires[realm]])
|
|
||||||
# On récupère le premier id libre dans la plages s'il n'est pas
|
# On récupère le premier id libre dans la plages s'il n'est pas
|
||||||
# déjà précisé dans le ldif
|
# déjà précisé dans le ldif
|
||||||
rid = uldif.setdefault('rid', [unicode(self._find_id('rid', plage)) ])
|
rid = uldif.setdefault('rid', [unicode(self._find_id('rid', realm))])
|
||||||
|
|
||||||
# La machine peut-elle avoir une ipv4 ?
|
# La machine peut-elle avoir une ipv4 ?
|
||||||
if 'v6' not in realm:
|
if 'v6' not in realm:
|
||||||
|
@ -307,9 +305,9 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
'''Crée une nouvelle entité ldap avec le dn ``dn`` et les
|
'''Crée une nouvelle entité ldap avec le dn ``dn`` et les
|
||||||
attributs de ``ldif``. Attention, ldif doit contenir des
|
attributs de ``ldif``. Attention, ldif doit contenir des
|
||||||
données encodées.'''
|
données encodées.'''
|
||||||
|
# Ajout des locks, on instancie les attributs qui ne sont pas
|
||||||
|
# des id, ceux-ci étant déjà lockés.
|
||||||
for key, values in uldif.iteritems():
|
for key, values in uldif.iteritems():
|
||||||
if key.endswith('id'):
|
|
||||||
continue
|
|
||||||
attribs = [attributs.attrify(val, key, self) for val in values]
|
attribs = [attributs.attrify(val, key, self) for val in values]
|
||||||
for attribut in attribs:
|
for attribut in attribs:
|
||||||
if attribut.unique:
|
if attribut.unique:
|
||||||
|
@ -323,12 +321,17 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
self.lockholder.removelock(key, str(attribut))
|
self.lockholder.removelock(key, str(attribut))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _find_id(self, attr, plage=None):
|
def _find_id(self, attr, realm=None):
|
||||||
'''Trouve un id libre. Si une plage est fournie, cherche
|
'''Trouve un id libre. Si une plage est fournie, cherche
|
||||||
l'id dans celle-ci, sinon, prend le plus élevé possible.'''
|
l'id dans celle-ci, sinon, prend le plus élevé possible.'''
|
||||||
res = self.search_s(variables.base_dn, ldap.SCOPE_SUBTREE, '%s=*' % attr, attrlist = [attr])
|
res = self.search_s(variables.base_dn, ldap.SCOPE_SUBTREE, '%s=*' % attr, attrlist = [attr])
|
||||||
nonfree = [ int(r[1].get(attr)[0]) for r in res if r[1].get(attr) ]
|
nonfree = [ int(r[1].get(attr)[0]) for r in res if r[1].get(attr) ]
|
||||||
nonfree.sort()
|
nonfree.sort()
|
||||||
|
|
||||||
|
plage = None
|
||||||
|
# On récupère la plage des mids
|
||||||
|
if realm != None:
|
||||||
|
plage = itertools.chain(*[xrange(a,b+1) for (a,b) in config.rid_primaires[realm]])
|
||||||
|
|
||||||
if plage != None:
|
if plage != None:
|
||||||
for i in plage:
|
for i in plage:
|
||||||
|
@ -342,6 +345,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.lockholder.addlock(attr, str(i))
|
self.lockholder.addlock(attr, str(i))
|
||||||
|
self.lockholder.removelock(attr, str(i))
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
@ -353,6 +357,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self.lockholder.addlock(attr, str(i))
|
self.lockholder.addlock(attr, str(i))
|
||||||
|
self.lockholder.removelock(attr, str(i))
|
||||||
break
|
break
|
||||||
except ldap_locks.LockError:
|
except ldap_locks.LockError:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
107
objets.py
107
objets.py
|
@ -190,6 +190,18 @@ class CransLdapObject(object):
|
||||||
faite"""
|
faite"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def check_changes(self):
|
||||||
|
"""
|
||||||
|
Vérifie la consistence d'un objet
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate_changes(self):
|
||||||
|
"""
|
||||||
|
Après vérification, harmonise l'objet
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def create(self, login=None):
|
def create(self, login=None):
|
||||||
"""Crée l'objet dans la base ldap, cette méthode vise à faire en sorte que
|
"""Crée l'objet dans la base ldap, cette méthode vise à faire en sorte que
|
||||||
l'objet se crée lui-même, si celui qui essaye de le modifier a les droits
|
l'objet se crée lui-même, si celui qui essaye de le modifier a les droits
|
||||||
|
@ -263,9 +275,12 @@ class CransLdapObject(object):
|
||||||
modlist = self.get_modlist()
|
modlist = self.get_modlist()
|
||||||
try:
|
try:
|
||||||
self.conn.modify_s(self.dn, modlist)
|
self.conn.modify_s(self.dn, modlist)
|
||||||
|
self.conn.lockholder.purge(id(self))
|
||||||
|
self.conn.lockholder.purge()
|
||||||
except:
|
except:
|
||||||
# On nettoie les locks
|
# On nettoie les locks
|
||||||
self.conn.lockholder.purge(id(self))
|
self.conn.lockholder.purge(id(self))
|
||||||
|
self.conn.lockholder.purge()
|
||||||
self._modifs = self.attrs
|
self._modifs = self.attrs
|
||||||
raise EnvironmentError("Impossible de modifier l'objet, peut-être n'existe-t-il pas ?")
|
raise EnvironmentError("Impossible de modifier l'objet, peut-être n'existe-t-il pas ?")
|
||||||
|
|
||||||
|
@ -350,15 +365,16 @@ class CransLdapObject(object):
|
||||||
# sert à permettre les vérifications de cardinalité
|
# sert à permettre les vérifications de cardinalité
|
||||||
# (on peut pas utiliser self._modifs, car il ne faut
|
# (on peut pas utiliser self._modifs, car il ne faut
|
||||||
# faire le changement que si on peut)
|
# faire le changement que si on peut)
|
||||||
|
|
||||||
attrs_before_verif = [ attributs.attrify(val, attr, self.conn, Parent=self) for val in values ]
|
attrs_before_verif = [ attributs.attrify(val, attr, self.conn, Parent=self) for val in values ]
|
||||||
if attr in self.attrs.keys():
|
if attr in self.attrs.keys():
|
||||||
for attribut in attrs_before_verif:
|
for attribut in attrs_before_verif:
|
||||||
attribut.check_uniqueness([content.value for content in self.attrs[attr]])
|
attribut.check_uniqueness([unicode(content) for content in self.attrs[attr]])
|
||||||
|
|
||||||
# On groupe les attributs précédents, et les nouveaux
|
# On groupe les attributs précédents, et les nouveaux
|
||||||
mixed_attrs = attrs_before_verif + self.attrs[attr]
|
mixed_attrs = attrs_before_verif + self.attrs[attr]
|
||||||
else:
|
else:
|
||||||
|
for attribut in attrs_before_verif:
|
||||||
|
attribut.check_uniqueness([])
|
||||||
mixed_attrs = attrs_before_verif
|
mixed_attrs = attrs_before_verif
|
||||||
# Si c'est vide, on fait pas de vérifs, on avait une liste
|
# Si c'est vide, on fait pas de vérifs, on avait une liste
|
||||||
# vide avant, puis on en a une nouvelle après.
|
# vide avant, puis on en a une nouvelle après.
|
||||||
|
@ -369,7 +385,11 @@ class CransLdapObject(object):
|
||||||
self._modifs[attr] = attrs_before_verif
|
self._modifs[attr] = attrs_before_verif
|
||||||
for attribut in attrs_before_verif:
|
for attribut in attrs_before_verif:
|
||||||
if attribut.unique:
|
if attribut.unique:
|
||||||
self.conn.lockholder.addlock(attr, str(attribut), id(self))
|
try:
|
||||||
|
self.conn.lockholder.addlock(attr, str(attribut), id(self))
|
||||||
|
except:
|
||||||
|
self._modifs[attr] = list(self.attrs[attr])
|
||||||
|
raise
|
||||||
|
|
||||||
def search_historique(self, ign_fields=HIST_IGNORE_FIELDS):
|
def search_historique(self, ign_fields=HIST_IGNORE_FIELDS):
|
||||||
u"""Récupère l'historique
|
u"""Récupère l'historique
|
||||||
|
@ -632,8 +652,85 @@ class machine(CransLdapObject):
|
||||||
if self._proprio._machines is not None:
|
if self._proprio._machines is not None:
|
||||||
self._proprio._machines.remove(self)
|
self._proprio._machines.remove(self)
|
||||||
|
|
||||||
|
def check_changes(self):
|
||||||
|
old = {}
|
||||||
|
new = {}
|
||||||
|
sbm = {'rid' : (), 'ipHostNumber' : (), 'ip6HostNumber' : ()}
|
||||||
|
default = {'rid': -1, 'ipHostNumber': u'', 'macAddress': u''}
|
||||||
|
for i in ['rid', 'ipHostNumber', 'macAddress']:
|
||||||
|
try:
|
||||||
|
old[i] = self.attrs[i][0].value
|
||||||
|
except:
|
||||||
|
old[i] = default[i]
|
||||||
|
try:
|
||||||
|
new[i] = self._modifs[i][0].value
|
||||||
|
except:
|
||||||
|
new[i] = default[i]
|
||||||
|
if old['rid'] != new['rid']:
|
||||||
|
nip4 = unicode(crans_utils.ip4_of_rid(new['rid']))
|
||||||
|
oip4 = unicode(new['ipHostNumber'])
|
||||||
|
if oip4 != nip4:
|
||||||
|
sbm['ipHostNumber'] = (oip4, nip4)
|
||||||
|
nip6 = unicode(crans_utils.ip6_of_mac(new['macAddress'], new['rid']))
|
||||||
|
try:
|
||||||
|
oip6 = unicode(self._modifs['ip6HostNumber'][0])
|
||||||
|
except:
|
||||||
|
oip6 = u""
|
||||||
|
if oip6 != nip6:
|
||||||
|
sbm['ip6HostNumber'] = (oip6, nip6)
|
||||||
|
return sbm
|
||||||
|
elif unicode(old['ipHostNumber']) != unicode(new['ipHostNumber']):
|
||||||
|
nrid = crans_utils.rid_of_ip4(new['ipHostNumber'])
|
||||||
|
orid = new['rid']
|
||||||
|
if nrid != orid:
|
||||||
|
sbm['rid'] = (orid, nrid)
|
||||||
|
return sbm
|
||||||
|
elif old['macAddress'] != new['macAddress']:
|
||||||
|
nip6 = unicode(crans_utils.ip6_of_mac(new['macAddress'], new['rid']))
|
||||||
|
try:
|
||||||
|
oip6 = unicode(self._modifs['ip6HostNumber'][0])
|
||||||
|
except:
|
||||||
|
oip6 = u""
|
||||||
|
if oip6 != nip6:
|
||||||
|
sbm['ip6HostNumber'] = (oip6, nip6)
|
||||||
|
return sbm
|
||||||
|
|
||||||
|
def validate_changes(self):
|
||||||
|
sbm = self.check_changes()
|
||||||
|
if sbm['rid']:
|
||||||
|
if sbm['rid'][1] == -1:
|
||||||
|
try:
|
||||||
|
ip6 = unicode(self._modifs['ip6HostNumber'][0])
|
||||||
|
except:
|
||||||
|
ip6 = u""
|
||||||
|
if ip6 != u"":
|
||||||
|
realm = crans_utils.find_rid_plage(sbm['rid'][0])[0]
|
||||||
|
if 'v6' not in realm:
|
||||||
|
realm = realm + "-v6"
|
||||||
|
self['rid'] = [unicode(self.conn._find_id('rid', realm))]
|
||||||
|
self['ip6HostNumber'] = [unicode(crans_utils.ip6_of_mac(self['macAddress'][0].value, self['rid'][0].value))]
|
||||||
|
else:
|
||||||
|
self['ipHostNumber'] = []
|
||||||
|
self['ip6HostNumber'] = []
|
||||||
|
else:
|
||||||
|
if unicode(self['ipHostNumber'][0]) != unicode(ip4_of_rid(sbm['rid'][1])):
|
||||||
|
raise ValueError("L'ipv4 et le rid ne concordent pas !")
|
||||||
|
self['ip6HostNumber'] = [unicode(crans_utils.ip6_of_mac(self['macAddress'][0].value, self['rid'][0].value))]
|
||||||
|
if sbm['ipHostNumber']:
|
||||||
|
if sbm['ipHostNumber'][1] == u"":
|
||||||
|
ip4 = []
|
||||||
|
else:
|
||||||
|
ip4 = sbm['ipHostNumber'][1]
|
||||||
|
self['ipHostNumber'] = ip4
|
||||||
|
if sbm['ip6HostNumber']:
|
||||||
|
if sbm['ip6HostNumber'][1] == u"":
|
||||||
|
ip6 = []
|
||||||
|
else:
|
||||||
|
ip6 = sbm['ip6HostNumber'][1]
|
||||||
|
self['ip6HostNumber'] = ip6
|
||||||
|
|
||||||
class AssociationCrans(proprio):
|
class AssociationCrans(proprio):
|
||||||
u""" Association crans (propriétaire particulier)."""
|
""" Association crans (propriétaire particulier)."""
|
||||||
def save(self):
|
def save(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -647,8 +744,6 @@ class BaseInvites(proprio):
|
||||||
u"""Un artefact de la base ldap"""
|
u"""Un artefact de la base ldap"""
|
||||||
def delete(self, comm, login):
|
def delete(self, comm, login):
|
||||||
raise EnvironmentError("Les pauvres invites")
|
raise EnvironmentError("Les pauvres invites")
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@crans_object
|
@crans_object
|
||||||
class adherent(proprio):
|
class adherent(proprio):
|
||||||
|
|
4
test.py
4
test.py
|
@ -201,8 +201,8 @@ except Exception:
|
||||||
else:
|
else:
|
||||||
print OK
|
print OK
|
||||||
|
|
||||||
tests_machines(adherent.dn, ["adherents", "fil-v6", "personnel-ens"])
|
tests_machines(adherent.dn, ["adherents", "adherents-v6", "personnel-ens"])
|
||||||
tests_machines(adherent.dn, ["wifi-adh", "wifi-v6"], ipsec=True)
|
tests_machines(adherent.dn, ["wifi-adh", "wifi-adh-v6"], ipsec=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue