summaryrefslogtreecommitdiffstats
path: root/src/com.gluster.storage.management.server.scripts
diff options
context:
space:
mode:
authorTim <timothyasir@gluster.com>2011-07-29 13:55:28 +0530
committerTim <timothyasir@gluster.com>2011-07-29 14:08:47 +0530
commit9e6dbf2f047598fc1701cad532653e25425da635 (patch)
tree9021adddcc1de4cfe021aa40242682e198a5f84c /src/com.gluster.storage.management.server.scripts
parentdeb48ac80e84e20536fdef151e91aaeeeff7ceb5 (diff)
Added functionalities to support CIFS.
Added add_user_cifs.py, add_user_cifs_all.py, create_volume_cifs.py Added delete_user_cifs.py, delete_user_cifs_all.py, delete_volume_cifs.py, grun.py Added modify_volume_cifs.py, setup_cifs_config.py, start_volume_cifs.py, stop_volume_cifs.py
Diffstat (limited to 'src/com.gluster.storage.management.server.scripts')
-rw-r--r--src/com.gluster.storage.management.server.scripts/src/Globals.py3
-rw-r--r--src/com.gluster.storage.management.server.scripts/src/VolumeUtils.py2
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/add_user_cifs.py37
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/add_user_cifs_all.py66
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/create_volume_cifs.py33
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/delete_user_cifs.py24
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/delete_user_cifs_all.py53
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/delete_volume_cifs.py32
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/grun.py36
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/modify_volume_cifs.py28
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/setup_cifs_config.py81
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/start_volume_cifs.py41
-rwxr-xr-xsrc/com.gluster.storage.management.server.scripts/src/stop_volume_cifs.py38
13 files changed, 473 insertions, 1 deletions
diff --git a/src/com.gluster.storage.management.server.scripts/src/Globals.py b/src/com.gluster.storage.management.server.scripts/src/Globals.py
index 5c1c4bee..f8a07c25 100644
--- a/src/com.gluster.storage.management.server.scripts/src/Globals.py
+++ b/src/com.gluster.storage.management.server.scripts/src/Globals.py
@@ -55,7 +55,7 @@ VOLUME_USER_DESCRIPTION = "Gluster Volume User"
SERVER_AGENT_RUN_USERNAME = "gluster"
INSTALLER_SERVER_NAME = "$installer$"
-GLUSTER_BASE_DIR = "/GLUSTER"
+GLUSTER_BASE_DIR = "/etc/glustermg"
GLUSTER_LUN_DIR = "/data"
REEXPORT_DIR = "/reexport"
NFS_EXPORT_DIR = "/nfs"
@@ -120,3 +120,4 @@ VERSION_DICTONARY = {}
##
AWS_WEB_SERVICE_URL = "http://169.254.169.254/latest"
+REAL_SAMBA_CONF_FILE = "/etc/samba/real.smb.conf"
diff --git a/src/com.gluster.storage.management.server.scripts/src/VolumeUtils.py b/src/com.gluster.storage.management.server.scripts/src/VolumeUtils.py
index a19ccd62..b1031ccc 100644
--- a/src/com.gluster.storage.management.server.scripts/src/VolumeUtils.py
+++ b/src/com.gluster.storage.management.server.scripts/src/VolumeUtils.py
@@ -120,8 +120,10 @@ def removeVolumeCifsConfiguration(volumeName):
volumeFile = "%s/%s.smbconf" % (Globals.VOLUME_CONF_DIR, volumeName)
try:
os.remove(volumeFile)
+ return True
except OSError, e:
log("Failed to remove file %s: %s" % (volumeFile, str(e)))
+ return False
def getVolumeListByPartitionName(partitionName):
diff --git a/src/com.gluster.storage.management.server.scripts/src/add_user_cifs.py b/src/com.gluster.storage.management.server.scripts/src/add_user_cifs.py
new file mode 100755
index 00000000..7b9650d1
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/add_user_cifs.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+
+def main():
+ if len(sys.argv) < 4:
+ sys.stderr.write("usage: %s UID USERNAME PASSWORD\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ uid = sys.argv[1]
+ userName = sys.argv[2]
+ password = sys.argv[3]
+
+ if Utils.runCommand("groupadd -g %s %s" % (uid, userName)) != 0:
+ Utils.log("failed to add group gid:%s, name:%s\n" % (uid, userName))
+ sys.exit(1)
+
+ command = ["useradd", "-c", Globals.VOLUME_USER_DESCRIPTION, "-M", "-d", "/", "-s", "/sbin/nologin", "-u", uid, "-g", uid, userName]
+ if Utils.runCommand(command) != 0:
+ Utils.log("failed to add user uid:%s, name:%s\n" % (uid, userName))
+ sys.exit(2)
+
+ if Utils.runCommand("smbpasswd -s -a %s" % userName,
+ input="%s\n%s\n" % (password, password)) != 0:
+ Utils.log("failed to set smbpassword of user uid:%s, name:%s\n" % (uid, userName))
+ sys.exit(3)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/add_user_cifs_all.py b/src/com.gluster.storage.management.server.scripts/src/add_user_cifs_all.py
new file mode 100755
index 00000000..e4b48658
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/add_user_cifs_all.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Utils
+
+
+defaultUid = 1024000
+cifsUserFile = "/etc/glustermg/.users.cifs"
+
+
+def getLastUid():
+ if not os.path.exists(cifsUserFile):
+ return defaultUid
+ try:
+ fp = open(cifsUserFile)
+ content = fp.read()
+ fp.close()
+ except IOError, e:
+ Utils.log("failed to read file %s: %s" % (cifsUserFile, str(e)))
+ return False
+
+ lines = content.strip().split()
+ if not lines:
+ return defaultUid
+ return int(lines[-1].split(":")[0])
+
+
+def setUid(uid, userName):
+ try:
+ fp = open(cifsUserFile, "a")
+ fp.write("%s:%s\n" % (uid, userName))
+ fp.close()
+ return True
+ except IOError, e:
+ Utils.log("failed to write file %s: %s" % (cifsUserFile, str(e)))
+ return False
+
+
+def main():
+ if len(sys.argv) < 4:
+ sys.stderr.write("usage: %s SERVER_FILE USERNAME PASSWORD\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ serverFile = sys.argv[1]
+ userName = sys.argv[2]
+ password = sys.argv[3]
+
+ uid = getLastUid()
+ if not uid:
+ sys.exit(10)
+
+ uid += 1
+
+ rv = Utils.runCommand("grun.py %s add_user_cifs.py %s %s %s" % (serverFile, uid, userName, password))
+ if rv == 0:
+ if not setUid(uid, userName):
+ sys.exit(11)
+ sys.exit(rv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/create_volume_cifs.py b/src/com.gluster.storage.management.server.scripts/src/create_volume_cifs.py
new file mode 100755
index 00000000..a81b165b
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/create_volume_cifs.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+import VolumeUtils
+
+def main():
+ if len(sys.argv) < 3:
+ sys.stderr.write("usage: %s VOLUME_NAME USER1 USER2 ...\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ volumeName = sys.argv[1]
+ userList = sys.argv[2:]
+
+ volumeMountDirName = "%s/%s" % (Globals.REEXPORT_DIR, volumeName)
+ try:
+ os.mkdir(volumeMountDirName)
+ except OSError, e:
+ Utils.log("failed creating %s: %s\n" % (volumeMountDirName, str(e)))
+ sys.exit(1)
+
+ if VolumeUtils.writeVolumeCifsConfiguration(volumeName, userList):
+ sys.exit(0)
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs.py b/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs.py
new file mode 100755
index 00000000..e5cda957
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Utils
+
+def main():
+ if len(sys.argv) < 2:
+ sys.stderr.write("usage: %s USERNAME\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ userName = sys.argv[1]
+
+ if Utils.runCommand("userdel %s" % userName) != 0:
+ Utils.log("failed to remove user name:%s\n" % userName)
+ sys.exit(1)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs_all.py b/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs_all.py
new file mode 100755
index 00000000..38dd8109
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/delete_user_cifs_all.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Utils
+
+
+cifsUserFile = "/etc/glustermg/.users.cifs"
+
+
+def removeUser(userName):
+ try:
+ fp = open(cifsUserFile)
+ content = fp.read()
+ fp.close()
+ except IOError, e:
+ Utils.log("failed to read file %s: %s" % (cifsUserFile, str(e)))
+ return False
+
+ try:
+ fp = open(cifsUserFile, "w")
+ lines = content.strip().split()
+ for line in lines:
+ if line.split(":")[1] == userName:
+ continue
+ fp.write("%s\n" % line)
+ fp.close()
+ except IOError, e:
+ Utils.log("failed to write file %s: %s" % (cifsUserFile, str(e)))
+ return False
+ return True
+
+
+def main():
+ if len(sys.argv) < 3:
+ sys.stderr.write("usage: %s SERVER_LIST USERNAME\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ serverList = sys.argv[1]
+ userName = sys.argv[2]
+
+ rv = Utils.runCommand("grun.py %s delete_user_cifs.py %s" % (serverList, userName))
+ if rv == 0:
+ if not removeUser(userName):
+ sys.exit(10)
+ sys.exit(rv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/delete_volume_cifs.py b/src/com.gluster.storage.management.server.scripts/src/delete_volume_cifs.py
new file mode 100755
index 00000000..fd1febc9
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/delete_volume_cifs.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+import VolumeUtils
+
+def main():
+ if len(sys.argv) != 2:
+ sys.stderr.write("usage: %s VOLUME_NAME\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ volumeName = sys.argv[1]
+
+ volumeMountDirName = "%s/%s" % (Globals.REEXPORT_DIR, volumeName)
+ try:
+ os.rmdir(volumeMountDirName)
+ except OSError, e:
+ Utils.log("failed deleting %s: %s\n" % (volumeMountDirName, str(e)))
+ sys.exit(1)
+
+ if VolumeUtils.removeVolumeCifsConfiguration(volumeName):
+ sys.exit(0)
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/grun.py b/src/com.gluster.storage.management.server.scripts/src/grun.py
new file mode 100755
index 00000000..ae93b7f2
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/grun.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Utils
+
+
+def main():
+ sshCommandPrefix = "ssh -q -o BatchMode=yes -o GSSAPIAuthentication=no -o PasswordAuthentication=no -o StrictHostKeyChecking=no".split()
+
+ if len(sys.argv) < 3:
+ sys.stderr.write("usage: %s SERVER_FILE COMMAND [ARGUMENTS]\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+ serverFile = sys.argv[1]
+ command = sys.argv[2:]
+
+ try:
+ fp = open(serverFile)
+ serverNameList = fp.readlines()
+ fp.close()
+ except IOError, e:
+ Utils.log("Failed to read server file %s: %s\n" % (serverFile, str(e)))
+ sys.exit(1)
+
+ for serverName in serverNameList:
+ rv = Utils.runCommand(sshCommandPrefix + [serverName.strip()] + command)
+ print rv
+
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/modify_volume_cifs.py b/src/com.gluster.storage.management.server.scripts/src/modify_volume_cifs.py
new file mode 100755
index 00000000..f6bacfc4
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/modify_volume_cifs.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Utils
+import VolumeUtils
+
+def main():
+ if len(sys.argv) <= 2:
+ sys.stderr.write("usage: %s VOLUME_NAME USER1 USER2 ...\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ volumeName = sys.argv[1]
+ userList = sys.argv[2:]
+
+ if not VolumeUtils.writeVolumeCifsConfiguration(volumeName, userList):
+ sys.exit(1)
+ if Utils.runCommand("service smb reload") != 0:
+ Utils.log("Failed to reload smb service")
+ sys.exit(2)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/setup_cifs_config.py b/src/com.gluster.storage.management.server.scripts/src/setup_cifs_config.py
new file mode 100755
index 00000000..2cc35acc
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/setup_cifs_config.py
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+
+def main():
+ try:
+ os.mkdir(Globals.GLUSTER_BASE_DIR)
+ os.mkdir(Globals.VOLUME_CONF_DIR)
+ os.mkdir(Globals.CIFS_EXPORT_DIR)
+ os.mkdir(Globals.REEXPORT_DIR)
+ except OSError, e:
+ Utils.log("failed to create directory: %s" % str(e))
+ sys.exit(1)
+ try:
+ fp = open(Globals.VOLUME_SMBCONF_FILE, "w")
+ fp.close()
+ except IOError, e:
+ Utils.log("Failed to create file %s: %s" % (Globals.VOLUME_SMBCONF_FILE, str(e)))
+ sys.exit(2)
+ try:
+ os.rename(Globals.SAMBA_CONF_FILE, "%s.orig" % Globals.SAMBA_CONF_FILE)
+ except IOError, e:
+ Utils.log("Ignoring rename %s to %s: %s" % (Globals.SAMBA_CONF_FILE, "%s.orig" % Globals.SAMBA_CONF_FILE, str(e)))
+ try:
+ fp = open(Globals.SAMBA_CONF_FILE, "w")
+ fp.write("##\n")
+ fp.write("## THIS FILE SHOULD NOT BE MODIFIED. IF YOU WANT TO MODIFY SAMBA\n")
+ fp.write("## CONFIGURATIONS, USE /etc/samba/real.smb.conf FILE\n")
+ fp.write("##\n")
+ fp.write("include = %s\n\n" % Globals.REAL_SAMBA_CONF_FILE)
+ fp.write("## CAUTION: DO NOT REMOVE BELOW LINE. REMOVAL OF THE LINE DISABLES\n")
+ fp.write("## CIFS REEXPORT OF GLUSTER VOLUMES\n")
+ fp.write("include = %s\n" % Globals.VOLUME_SMBCONF_FILE)
+ fp.close()
+ except IOError, e:
+ Utils.log("Failed to create samba configuration file %s: %s" % (Globals.SAMBA_CONF_FILE, str(e)))
+ sys.exit(3)
+ try:
+ fp = open(Globals.REAL_SAMBA_CONF_FILE, "w")
+ fp.write("[global]\n")
+ fp.write("## CAUTION: DO NOT REMOVE BELOW INCLUDE LINE. REMOVAL OF THE LINE\n")
+ fp.write("## DISABLES SERVER/CIFS HIGH AVAILABILITY\n")
+ #fp.write("include = %s\n" % Globals.CTDB_SAMBA_CONF_FILE)
+ fp.write("##\n")
+ fp.write("socket options = TCP_NODELAY IPTOS_LOWDELAY SO_SNDBUF=131072 SO_RCVBUF=131072\n")
+ fp.write("read raw = yes\n")
+ fp.write("server string = %h\n")
+ fp.write("write raw = yes\n")
+ fp.write("oplocks = yes\n")
+ fp.write("max xmit = 131072\n")
+ fp.write("dead time = 15\n")
+ fp.write("getwd cache = yes\n")
+ fp.write("#read size = 131072\n")
+ fp.write("use sendfile=yes\n")
+ fp.write("block size = 131072\n")
+ fp.write("printcap name = /etc/printcap\n")
+ fp.write("load printers = no\n")
+ fp.close()
+ except IOError, e:
+ Utils.log("Failed to create samba configuration file %s: %s" % (Globals.REAL_SAMBA_CONF_FILE, str(e)))
+ sys.exit(4)
+
+
+ if Utils.runCommand("setsebool -P samba_share_fusefs on") != 0:
+ Utils.log("failed to set SELinux samba_share_fusefs")
+ sys.exit(5)
+
+ if Utils.runCommand("service smb restart") != 0:
+ Utils.log("failed to restart smb service")
+ sys.exit(6)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/start_volume_cifs.py b/src/com.gluster.storage.management.server.scripts/src/start_volume_cifs.py
new file mode 100755
index 00000000..239216c3
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/start_volume_cifs.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+import VolumeUtils
+
+def main():
+ if len(sys.argv) != 2:
+ sys.stderr.write("usage: %s VOLUME_NAME\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ volumeName = sys.argv[1]
+
+ volumeMountDirName = "%s/%s" % (Globals.REEXPORT_DIR, volumeName)
+ cifsDirName = "%s/%s" % (Globals.CIFS_EXPORT_DIR, volumeName)
+
+ if Utils.runCommand("mount -t glusterfs 127.0.0.1:%s %s" % (volumeName, volumeMountDirName)) != 0:
+ Utils.log("Failed to mount volume %s" % (volumeName))
+ sys.exit(1)
+ if Utils.runCommand("ln -fTs %s %s" % (volumeMountDirName, cifsDirName)) != 0:
+ Utils.log("Failed to create reexport link %s" % cifsDirName)
+ sys.exit(2)
+ if Utils.runCommand("chcon -t samba_share_t %s -h" % cifsDirName) != 0:
+ Utils.log("Failed to change security context for the link %s" % cifsDirName)
+ sys.exit(2)
+ if not VolumeUtils.includeVolume(volumeName):
+ Utils.log("Failed to include volume for CIFS reexport")
+ sys.exit(3)
+ if Utils.runCommand("service smb reload") != 0:
+ Utils.log("Failed to reload smb service")
+ sys.exit(4)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/com.gluster.storage.management.server.scripts/src/stop_volume_cifs.py b/src/com.gluster.storage.management.server.scripts/src/stop_volume_cifs.py
new file mode 100755
index 00000000..99ac4750
--- /dev/null
+++ b/src/com.gluster.storage.management.server.scripts/src/stop_volume_cifs.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+# Copyright (C) 2011 Gluster, Inc. <http://www.gluster.com>
+# This file is part of Gluster Management Gateway.
+#
+
+import os
+import sys
+import Globals
+import Utils
+import VolumeUtils
+
+def main():
+ if len(sys.argv) != 2:
+ sys.stderr.write("usage: %s VOLUME_NAME\n" % os.path.basename(sys.argv[0]))
+ sys.exit(-1)
+
+ volumeName = sys.argv[1]
+
+ volumeMountDirName = "%s/%s" % (Globals.REEXPORT_DIR, volumeName)
+ cifsDirName = "%s/%s" % (Globals.CIFS_EXPORT_DIR, volumeName)
+
+ if not Utils.removeFile(cifsDirName):
+ Utils.log("Failed to remove reexport link %s" % cifsDirName)
+ sys.exit(1)
+ if not VolumeUtils.excludeVolume(volumeName):
+ Utils.log("Failed to exclude volume for CIFS reexport")
+ sys.exit(2)
+ if Utils.runCommand("service smb reload") != 0:
+ Utils.log("Failed to reload smb service")
+ sys.exit(3)
+ if Utils.runCommand("umount %s" % (volumeMountDirName)) != 0:
+ Utils.log("Failed to unmount volume %s" % (volumeName))
+ sys.exit(4)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()