develop
asamoukin 16 years ago
parent 24d929c6a2
commit 03de2e466a

@ -10,8 +10,8 @@ homeDirectory: #-soft_ldap_user_home-#
loginShell: #-soft_ldap_user_shell-#
# число дней с 1970 года в течении которых будет действовать пароль
shadowExpire: -1
# зарезервированный аттрибут
shadowFlag: 0
# зарезервированный аттрибут (У нас это видимость пользователя другим компьютером)
shadowFlag: #-soft_ldap_user_visible-#
# число дней, после устаревания пароля для блокировки учётной записи
shadowInactive: -1
# Дата последнего измения пароля в днях с 1970 года (26 августа 2008 года)

@ -103,7 +103,7 @@ chown=root:root
valid users = %U @"Domain Admins"
[netlogon]
path = #-soft_ldap_samba_netlogon_path-#
path = #-soft_ldap_samba_netlogon_path-#/%u
browseable = No
read only = yes

@ -171,6 +171,49 @@ class shareLdap(imp_cl_err, imp_cl_xml, imp_cl_help, imp_cl_smcon):
# DN сервисов относительно базового
self.ServicesDN = "ou=Services"
@adminConnectLdap
def backupDelUser(self, userName, service, srcDir):
"""Сохраняем данные удаляемого пользователя"""
# Ищем Unix пользователя
searchUnixUser = self.servUnixObj.searchUnixUser(userName)
# id пользователя
strUid = ""
if searchUnixUser:
strUid = searchUnixUser[0][0][1]['uidNumber'][0]
if strUid:
delBackDir =\
os.path.join(self.clVars.Get("soft_ldap_delete_user_dir"),
"%s-%s"%(userName,strUid),
service)
if os.path.exists(delBackDir) and os.listdir(delBackDir):
self.printERROR(_("Found deleted user data dir %s")\
%delBackDir)
self.printERROR(_("Not created deleted user data dir %s")\
%delBackDir)
return False
else:
delBackDir =\
os.path.join(self.clVars.Get("soft_ldap_delete_user_dir"),
"%s"%(userName),
service)
i = 0
while os.path.exists(delBackDir):
delBackDir =\
os.path.join(self.clVars.Get("soft_ldap_delete_user_dir"),
"%s_%s"%(userName,i),
service)
i += 1
#Делаем сохранение почтовой директории
self.copyDir(delBackDir, srcDir)
if os.path.exists(delBackDir):
self.printSUCCESS(_("Created deleted user data dir %s")\
%delBackDir)
return True
else:
self.printERROR(_("Not created deleted user data dir %s")\
%delBackDir)
return False
def stringIsJpeg(self, string):
"""Определяет является ли строка jpeg изображением"""
if len(string)<8:
@ -464,6 +507,54 @@ class shareLdap(imp_cl_err, imp_cl_xml, imp_cl_help, imp_cl_smcon):
return False
return True
def copyDir(self, destDir, srcDir):
"""Копируем директорию в другое место
При копировании сохраняются владелец, группа, права
"""
if not os.path.exists(destDir):
# Создаем домашнюю директорию
os.makedirs(destDir)
# Файловый объект
fileObj = cl_profile._file()
# Сканируем скелетную директорию
scanObjs = fileObj.scanDirs([srcDir])
if not scanObjs:
return True
scanObjs[0].dirs.sort(lambda x, y: cmp(len(y), len(x)))
for dirSrc in scanObjs[0].dirs:
#создаем в домашней директории директории из srcDir
dirName = destDir + dirSrc.split(srcDir)[1]
os.mkdir(dirName)
mode,uid,gid = fileObj.getModeFile(dirSrc)
os.chown(dirName, uid,gid)
os.chmod(destDir, mode)
for fileCopy in scanObjs[0].files:
oldFile = destDir + fileCopy.split(srcDir)[1]
#копируем файлы
fileObj.openFiles(fileCopy, oldFile)
fileObj.saveOldFile()
fileObj.oldProfile = False
fileObj.closeFiles()
os.chown(oldFile, fileObj._uid, fileObj._gid)
os.chmod(oldFile, fileObj._mode)
for linkCreate in scanObjs[0].links:
#копируем ссылки
dst = destDir + linkCreate[1].split(srcDir)[1]
srcDestList = linkCreate[0].split(srcDir)
if len(srcDestList)>1:
src = destDir + srcDestList[1]
else:
src = linkCreate[0]
os.symlink(src,dst)
mode,uid,gid = fileObj.getModeFile(linkCreate)
#Изменение прав на ссылки
os.lchown(dst, uid, gid)
mode,uid,gid = fileObj.getModeFile(srcDir)
os.chmod(destDir, mode)
os.chown(destDir, uid,gid)
return True
def addDN(self, *arg):
"""Складывает текстовые элементы DN"""
DNs = []
@ -1130,8 +1221,14 @@ class servUnix(shareLdap):
return True
@adminConnectLdap
def addUserUnixServer(self,userName,options):
def addUserUnixServer(self,userName,options, printSuccess=True):
"""Добавляет Unix пользователя в LDAP-сервер"""
if self.searchUnixUser(userName):
self.printERROR(_("User exists in Unix service"))
return False
elif self.searchPasswdUser(userName):
self.printERROR(_("User exists in") + " /etc/passwd")
return False
# id нового пользователя
userId = str(self.getMaxUid())
self.clVars.Set("soft_ldap_user_login", userName)
@ -1156,16 +1253,13 @@ class servUnix(shareLdap):
if options.has_key('c'):
fullNameUser = options['c']
self.clVars.Set("soft_ldap_user_full_name",fullNameUser)
if self.searchUnixUser(userName):
self.printERROR(_("User exists in Unix service"))
return False
elif self.searchPasswdUser(userName):
self.printERROR(_("User exists in") + " /etc/passwd")
return False
userShell = self.userShell
# По умолчанию пользователя не видно
visible = '0'
if options.has_key('v'):
visible = '1'
self.clVars.Set("soft_ldap_user_visible",visible)
# Оболочка пользователя
userShell = self.userShell
if options.has_key('s'):
userShell = options['s']
self.clVars.Set("soft_ldap_user_shell", userShell)
@ -1296,17 +1390,61 @@ class servUnix(shareLdap):
self.delUserUnixServer(userName, {'r':""}, False)
else:
self.delUserUnixServer(userName, {}, False)
self.printERROR (_("Cannot added user")+ " " + str(userName))
self.printERROR (_("Can not added user")+ " " + str(userName))
return False
if flagAdd.has_key('group'):
self.printSUCCESS(_("Added group in Unix service") + " ...")
if printSuccess:
self.printSUCCESS(_("Added group in Unix service") + " ...")
if options.has_key('m'):
self.printSUCCESS(_("Created home dir %s")% homeDir+\
if printSuccess:
self.printSUCCESS(_("Created home dir %s")% homeDir+\
" ...")
if options.has_key('i'):
self.printSUCCESS(_("Added jpeg photo: %s")% photoFile+\
if printSuccess:
self.printSUCCESS(_("Added jpeg photo: %s")% photoFile+\
" ...")
self.printSUCCESS(_("Added user in Unix service") + " ...")
if printSuccess:
self.printSUCCESS(_("Added user in Unix service") + " ...")
return True
def setUserMail(self, userName, mail):
"""Устанавливает для пользователя основной почтовый адрес"""
searchUser = self.searchUnixUser(userName)
if not searchUser:
self.printERROR(_("User %s not found in Unix service")\
%str(userName))
return False
modAttrs = []
if searchUser[0][0][1].has_key('mail'):
modAttrs.append((ldap.MOD_REPLACE, 'mail', mail))
else:
modAttrs.append((ldap.MOD_ADD, 'mail', mail))
userDN = self.addDN('uid='+userName,self.relUsersDN)
if not self.modAttrsDN(userDN, modAttrs):
self.printERROR(_("Can not modify mail attribute in Unix service"))
return False
return True
def getUserMail(self, userName):
"""Выдаем основной почтовый адрес"""
searchUser = self.searchUnixUser(userName)
if not searchUser:
self.printERROR(_("User %s not found in Unix service")\
%str(userName))
return False
if searchUser[0][0][1].has_key('mail'):
return searchUser[0][0][1]['mail'][0]
else:
return ""
def deleteUserMail(self, userName):
if self.getUserMail(userName):
modAttrs =[(ldap.MOD_DELETE, 'mail', None)]
userDN = self.addDN('uid='+userName,self.relUsersDN)
if not self.modAttrsDN(userDN, modAttrs):
self.printERROR(_("Can not delete mail attribute in Unix \
service"))
return False
return True
def addUsersGroupUnix(self, users, groupName):
@ -1620,6 +1758,15 @@ class servUnix(shareLdap):
if resLdap:
userGid = resLdap[0][0][1]['gidNumber'][0]
modAttrs += [(ldap.MOD_REPLACE, 'gidNumber', userGid)]
visible = False
# пользователя видно
if options.has_key('V'):
visible = '1'
# пользователя не видно
if options.has_key('I'):
visible = '0'
if visible:
modAttrs += [(ldap.MOD_REPLACE, 'shadowFlag', visible)]
# Изменяем домашнюю директорию
if options.has_key('d'):
homeDir = options['d']
@ -1687,6 +1834,12 @@ class servUnix(shareLdap):
if options.has_key('U'):
self.printSUCCESS(_("Unlocked user %s")% str(userName) +\
" ...")
if options.has_key('I'):
self.printSUCCESS(_("User %s is invisible")% str(userName) +\
" ...")
if options.has_key('V'):
self.printSUCCESS(_("User %s is visible")% str(userName) +\
" ...")
if options.has_key('L'):
self.printSUCCESS(_("Locked user %s")% str(userName) +\
" ...")
@ -2037,7 +2190,7 @@ class servMail(shareLdap):
return False
return userGroupNames
def delUserMailServer(self, userName, options):
def delUserMailServer(self,userName,options,printSuccess=True,backup=True):
"""Удаляем Mail пользователя"""
# Ищем Mail пользователя
resSearch = self.searchMailUserToName(userName)
@ -2046,6 +2199,13 @@ class servMail(shareLdap):
_("User %s is not found in Mail service") % str(userName) +\
" ...")
return False
#почтовая директория пользователя
mailDir = os.path.join(self.clVars.Get("soft_ldap_mail_path"),
userName)
# Делаем сохранение данных удаляемого пользователя
if backup and os.listdir(mailDir):
if not self.backupDelUser(userName, 'mail', mailDir):
return False
# Удаляем пользователя из групп
if not self.delUserInGroup(userName):
return False
@ -2054,15 +2214,13 @@ class servMail(shareLdap):
if not self.delDN(delDN):
return False
# Удаляем почтовую папку
if options.has_key('r'):
#почтовая директория пользователя
mailDir = os.path.join(self.clVars.Get("soft_ldap_mail_path"),
userName)
if self.servUnixObj.removeHomeDir(mailDir):
if self.servUnixObj.removeHomeDir(mailDir):
if printSuccess:
self.printSUCCESS(\
_("Mail user directory %s is removed")% str(mailDir) +\
" ...")
self.printSUCCESS(_("Mail user %s is deleted")%userName +\
" ...")
if printSuccess:
self.printSUCCESS(_("Mail user %s is deleted")%userName +\
" ...")
return True
@ -2149,7 +2307,7 @@ class servMail(shareLdap):
groupDN = self.addDN("cn="+groupName, self.relGroupsDN)
return self.modAttrsDN(groupDN, modAttrs)
def delGroupMailServer(self, groupName, options, printSuccess=True):
def delGroupMailServer(self, groupName, options):
"""Удаляет группу пользователей Mail"""
res = self.searchMailGroupToName(groupName)
if not res:
@ -2160,8 +2318,7 @@ class servMail(shareLdap):
delDN = self.addDN("cn="+groupName, self.relGroupsDN)
res = self.delDN(delDN)
if res:
if printSuccess:
self.printSUCCESS( _("Mail group %s is deleted")%groupName + \
self.printSUCCESS( _("Mail group %s is deleted")%groupName + \
" ...")
return True
else:
@ -2350,6 +2507,8 @@ class servMail(shareLdap):
modAttrs.append((ldap.MOD_ADD, 'userPassword',
userPwdHash))
# Заменяем альтернативные почтовые адреса
# Первичный почтовый адрес
primaryMail = ""
if options.has_key('e'):
# Удаляем предыдущие адреса
self.delAlternateAddress(userName)
@ -2361,6 +2520,8 @@ class servMail(shareLdap):
mail = "%s@%s.%s" %(altMail,
self.clVars.Get("net_host"),
self.clVars.Get("sys_domain"))
if not primaryMail:
primaryMail = mail
if self.searchUserToMail(mail) or\
self.searchGroupToMail(mail):
self.printERROR(_("Alternate email address") + ": " +\
@ -2368,7 +2529,12 @@ class servMail(shareLdap):
" ...")
return False
modAttrs.append((ldap.MOD_ADD, 'mailAlternateAddress', mail))
# Изменяем основной почтовый адрес
if primaryMail:
if not self.servUnixObj.setUserMail(userName, primaryMail):
self.printERROR(_("Failed set primary email for user %s \
in Unix service ...") %str(primaryMail))
return False
if modAttrs:
DN = self.addDN("uid="+userName, self.relUsersDN)
if not self.modAttrsDN(DN, modAttrs):
@ -2408,7 +2574,7 @@ class servMail(shareLdap):
_("Alternate email address %s is found in Mail service")%\
str(mail) + " ...")
return False
modAttrs.append((ldap.MOD_ADD, 'mailAlternateAddress', mail))
modAttrs.append('mailAlternateAddress: %s' %mail)
if self.searchMailGroupToName(groupName):
self.printERROR(
_("group name %s is found in Mail service")%\
@ -2430,21 +2596,16 @@ class servMail(shareLdap):
groupGecos = options['c']
self.clVars.Set("soft_ldap_group_desc",groupGecos)
ldifFile = self.ldifFileGroup
groupLdif = self.createLdif(ldifFile)
if not groupLdif:
groupRawLdif = self.createLdif(ldifFile)
if not groupRawLdif:
print self.getError()
return False
groupLdif = groupRawLdif.rstrip() + "\n" + "\n".join(modAttrs)
if not self.ldapObj.getError():
self.ldapObj.ldapAdd(groupLdif)
if self.ldapObj.getError():
print _("LDAP Error") + ": " + self.ldapObj.getError().strip()
return False
#Добавляем альтернативные почтовые адреса
if options.has_key('e') and modAttrs:
DN = self.addDN("cn="+groupName, self.relGroupsDN)
if not self.modAttrsDN(DN, modAttrs):
self.delGroupMailServer(groupName, {}, False)
return False
self.printSUCCESS(_("Added group in Mail service") + " ...")
return True
@ -2502,7 +2663,10 @@ class servMail(shareLdap):
os.makedirs(mailDir)
os.chmod(mailDir,0700)
os.chown(mailDir,uid,gid)
return True
return True
else:
self.printERROR(_("Path %s exists") %mailDir)
return False
def searchUsersInGroupMail(self, usersNames, groupName):
"""Ищет спиcок пользователей в группе, ищет в LDAP
@ -2596,6 +2760,7 @@ class servMail(shareLdap):
"""Добавляет почтового пользователя в LDAP-сервер"""
#Проверяем альтернативные почтовые адреса
modAttrs = []
primaryMail = ""
if options.has_key('e'):
altMails = options['e'].split(",")
for altMail in altMails:
@ -2605,13 +2770,20 @@ class servMail(shareLdap):
mail = "%s@%s.%s" %(altMail,
self.clVars.Get("net_host"),
self.clVars.Get("sys_domain"))
if not primaryMail:
primaryMail = mail
if self.searchUserToMail(mail) or\
self.searchGroupToMail(mail):
self.printERROR(
_("Alternate email address %s is found in Mail service")%\
str(mail) + " ...")
return False
modAttrs.append((ldap.MOD_ADD, 'mailAlternateAddress', mail))
modAttrs.append("mailAlternateAddress: %s" %mail)
else:
self.printERROR(\
_("Must be added one or more alternative addresses"))
self.printWARNING("cl-useradd -e gst guest mail")
return False
if self.searchMailUserToName(userName):
self.printERROR(_("User exists in Mail service"))
return False
@ -2631,8 +2803,10 @@ class servMail(shareLdap):
userPwd = self.getUserPassword(options, "p", "P")
if userPwd == False:
return False
flagCreateUnixUser = False
if not (resUnix or resPwd):
if options.has_key('f'):
flagCreateUnixUser = True
# Добавим пользователя LDAP
optUnix = {}
# Группа пользователя
@ -2641,7 +2815,8 @@ class servMail(shareLdap):
# Полное имя пользователя
if options.has_key('c'):
optUnix['c'] = options['c']
if not self.servUnixObj.addUserUnixServer(userName, optUnix):
if not self.servUnixObj.addUserUnixServer(userName, optUnix,
False):
return False
resUnix = self.servUnixObj.searchUnixUser(userName)
else:
@ -2666,10 +2841,18 @@ class servMail(shareLdap):
if not userPwdHash:
self.printERROR(_("ERROR") + ": " +\
_("create crypto password"))
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
return False
self.clVars.Set("soft_ldap_user_pw_hash",userPwdHash)
ldifFile = self.ldifFileUser
userLdif = self.createLdif(ldifFile)
userRawLdif = self.createLdif(ldifFile)
if not userRawLdif:
print self.getError()
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
return False
userLdif = userRawLdif.rstrip() + "\n" + "\n".join(modAttrs)
if not self.ldapObj.getError():
#Добавляем пользователя в LDAP
self.ldapObj.ldapAdd(userLdif)
@ -2677,6 +2860,8 @@ class servMail(shareLdap):
# не переделывать на else
if self.ldapObj.getError():
print _("LDAP Error") + ": " + self.ldapObj.getError().strip()
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
return False
if resUnix:
uid = int(resUnix[0][0][1]['uidNumber'][0])
@ -2686,13 +2871,27 @@ class servMail(shareLdap):
gid = int(resPwd.split(":")[3])
else:
self.printERROR(_("user are not found"))
return False
self.createMailDir(userName, uid, gid)
#Добавляем альтернативные почтовые адреса
if options.has_key('e') and modAttrs:
DN = self.addDN("uid="+userName, self.relUsersDN)
if not self.modAttrsDN(DN, modAttrs):
return False
self.delUserMailServer(userName, {}, False, False)
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
return False
# Создаем почтовую директорию
if not self.createMailDir(userName, uid, gid):
self.delUserMailServer(userName, {}, False,False)
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
return False
# Записываем основной почтовый адрес
if primaryMail:
if not self.servUnixObj.setUserMail(userName, primaryMail):
self.delUserMailServer(userName, {}, False, False)
if flagCreateUnixUser:
self.servUnixObj.delUserUnixServer(userName, {}, False)
self.printERROR(_("Failed set primary email for user %s in\
Unix service ...") %str(primaryMail))
return False
if flagCreateUnixUser:
self.printSUCCESS(_("Added user in Unix service") + " ...")
self.printSUCCESS(_("Added user in Mail service") + " ...")
return True
@ -3463,11 +3662,9 @@ class servSamba(shareLdap):
"""Cоединение с LDAP администратором Samba сервиса"""
return shareLdap.getLdapObjInFile(self, "samba")
def delUserSambaServer(self, userName, options):
def delUserSambaServer(self,userName,options,printSuccess=True,
backup=True):
"""Удаляем Samba пользователя"""
if options.has_key('r'):
self.printERROR (_("Option 'r' is not valid for Samba service"))
return False
if "$" in userName:
# удаляемая машина
delUser = userName.replace('$','') + "$"
@ -3484,7 +3681,39 @@ class servSamba(shareLdap):
_("Samba user %s is not found in Samba service") %\
str(delUser))
return False
textLine = self.execProg("smbpasswd -x %s" %(delUser),False,False)
# Делаем сохранение данных удаляемого пользователя
if backup:
userProfDir =\
os.path.join(self.clVars.Get("soft_ldap_samba_profile_path"),
userName)
userHomeDir =\
os.path.join(self.clVars.Get("soft_ldap_samba_home_path"),
userName)
userNetlogonDir =\
os.path.join(self.clVars.Get("soft_ldap_samba_netlogon_path"),
userName)
if os.path.exists(userProfDir) and os.listdir(userProfDir):
if not self.backupDelUser(userName, 'samba/profile',
userProfDir):
return False
else:
self.printWARNING(_("Samba profile directory not found for \
user %s") %str(userName))
if os.path.exists(userHomeDir)and os.listdir(userHomeDir):
if not self.backupDelUser(userName, 'samba/home',
userHomeDir):
return False
else:
self.printWARNING(_("Samba home directory not found for \
user %s") %str(userName))
if os.path.exists(userNetlogonDir) and os.listdir(userNetlogonDir):
if not self.backupDelUser(userName, 'samba/netlogon',
userProfDir):
return False
else:
self.printWARNING(_("Samba netlogon directory not found for \
user %s") %str(userName))
textLine = self.execProg("smbpasswd -x %s" %(delUser), False, False)
flagError = False
if textLine:
if type(textLine) == types.ListType:
@ -3544,7 +3773,10 @@ class servSamba(shareLdap):
# Полное имя пользователя
if options.has_key('c'):
optUnix['c'] = options['c']
if not self.servUnixObj.addUserUnixServer(userName, optUnix):
# Cделаем пользователя видимым
optUnix['v'] = ""
if not self.servUnixObj.addUserUnixServer(userName, optUnix,
False):
return False
if userPwd:
textLine = self.execProg("smbpasswd -a -s %s" %(userName),
@ -3552,9 +3784,13 @@ class servSamba(shareLdap):
else:
textLine = self.execProg("smbpasswd -a -n %s" %(userName))
if "Added" in str(textLine):
if not resSearch:
self.printSUCCESS(_("Added user in Unix service") + " ...")
self.printSUCCESS(_("Added user in Samba service") +" ...")
return True
else:
if not resSearch:
self.servUnixObj.delUserUnixServer(userName, {}, False)
self.printERROR(_("Can not add user") + " ...")
return False
@ -4684,6 +4920,12 @@ class cl_ldap(shareLdap):
'helpChapter':_("Unix service options"),
'help':_("force use the UID for the new user account")
},
{'progAccess':(3,),
'shortOption':"v",
'longOption':"visible",
'helpChapter':_("Unix service options"),
'help':_("the new user account is visible (default - invisible)")
},
{'progAccess':(4,),
'shortOption':"r",
'longOption':"remove",
@ -4819,6 +5061,18 @@ class cl_ldap(shareLdap):
'helpChapter':_("Common options"),
'help':_("unlock the user account")
},
{'progAccess':(5,),
'shortOption':"V",
'longOption':"visible",
'helpChapter':_("Unix service options"),
'help':_("the user account is visible")
},
{'progAccess':(5,),
'shortOption':"I",
'longOption':"invisible",
'helpChapter':_("Unix service options"),
'help':_("the user account is invisible")
},
#{'progAccess':(5,),
#'shortOption':"u",
#'longOption':"uid",

@ -146,6 +146,10 @@ class Data:
'type':('param','soft'),
'value':'Computers',
}
# Видимость пользователя с другого компьютера
soft_ldap_user_visible= {'mode':"w",
'type':('param','soft'),
}
#-----------------------------------------------------
#Все сервисы Unix
#-----------------------------------------------------
@ -167,6 +171,11 @@ class Data:
soft_ldap_setup_name= {'mode':"w",
'type':('param','soft'),
}
#директория куда будут записаны данные удаленных пользователей
soft_ldap_delete_user_dir= {'mode':"w",
'type':('param','soft'),
'value':'/var/calculate/delete'
}
#-----------------------------------------------------
#Сервис Unix
#-----------------------------------------------------
@ -218,7 +227,7 @@ class Data:
# Домашняя директория
soft_ldap_samba_home_path = {'mode':"r",
'type':('param','soft'),
'value':'/var/calculate/services/samba/home'
'value':'/var/calculate/services/samba/share'
}
# Директория netlogon
soft_ldap_samba_netlogon_path = {'mode':"r",

@ -30,7 +30,7 @@ var_data_files = [("/var/calculate/profile/server",[]),
("/var/calculate/services",[]),
# samba
("/var/calculate/services/samba",[]),
("/var/calculate/services/samba/home",[]),
#("/var/calculate/services/samba/home",[]),
("/var/calculate/services/samba/share",[]),
("/var/calculate/services/samba/profiles",[]),
("/var/calculate/services/samba/netlogon",[]),

Loading…
Cancel
Save