Andrew Bogott has submitted this change and it was merged.

Change subject: Add manage-nfs-volumes-daemon
......................................................................


Add manage-nfs-volumes-daemon

This is the short-term future of labs storage.

Change-Id: Ied54fcd71ae20377d62beb21838b436fd2235212
---
A files/ldap/scripts/manage-nfs-volumes-daemon
A files/upstart/manage-nfs-volumes.conf
M manifests/ldap.pp
M manifests/openstack.pp
M manifests/site.pp
5 files changed, 364 insertions(+), 1 deletion(-)

Approvals:
  Andrew Bogott: Verified; Looks good to me, approved
  jenkins-bot: Verified



diff --git a/files/ldap/scripts/manage-nfs-volumes-daemon 
b/files/ldap/scripts/manage-nfs-volumes-daemon
new file mode 100755
index 0000000..9bf4b8a
--- /dev/null
+++ b/files/ldap/scripts/manage-nfs-volumes-daemon
@@ -0,0 +1,331 @@
+#!/usr/bin/python
+
+#####################################################################
+### THIS FILE IS MANAGED BY PUPPET
+### puppet:///files/ldap/scripts/manage-nfs-volumes
+#####################################################################
+
+import datetime
+import ldapsupportlib
+from optparse import OptionParser
+import os
+import socket
+import subprocess
+import sys
+import time
+import traceback
+
+try:
+    import ldap
+    import ldap.modlist
+except ImportError:
+    sys.stderr.write("Unable to import LDAP library.\n")
+
+NONE = 0
+INFO = 10
+DEBUG = 20
+
+
+class VolumeManager:
+    def __init__(self):
+        # Turn on for testing
+        self.dry_run = True
+
+        # TODO: Pull this info from a configuration file
+        self.base_dir = '/srv/'
+        self.base_snapshot_dir = '/time/'
+        self.base_export_volume_dir = '/exp/'
+        self.export_snapshot_dir = '.snapshot'
+        self.base_exports_dir = '/etc/exports.d/'
+        # Volumes in projects listed as global; so: { 'dumps': ['xml'] } would 
be
+        # an xml share in the dumps project being listed as global.
+        self.global_shares = {'publicdata': ['project']}
+        # Volumes which need to have hosts manually added to the export list; 
so, { 'dumps':
+        # { 'project': ['10.0.0.1'] } } would manually add 10.0.0.1 to the 
dumps-project access list
+        self.manual_shares = {'publicdata': {'project': ['208.80.154.11']}}
+        self.volume_names = ['home', 'project']
+        self.loglevel = INFO
+        self.logfile = None
+
+    def run(self):
+        parser = OptionParser(conflict_handler="resolve")
+        parser.set_usage('manage-volumes [options]')
+
+        self.ldapSupportLib = ldapsupportlib.LDAPSupportLib()
+        self.ldapSupportLib.addParserOptions(parser)
+
+        parser.add_option("--logfile", dest="logfile", help="Write output to 
the specified log file. (default: stdin)")
+        parser.add_option("--loglevel", dest="loglevel", help="Change level of 
logging; NONE, INFO, DEBUG (default: INFO)")
+        (options, args) = parser.parse_args()
+        self.ldapSupportLib.setBindInfoByOptions(options, parser)
+
+        if options.logfile:
+            self.logfile = options.logfile
+        if options.loglevel:
+            self.loglevel = options.loglevel
+
+        while True:
+            self.refresh_volumes()
+            time.sleep(180)
+
+    # Updates a member var:
+    #
+    # self.project_volumes:  A dict of existing volumes of the form
+    #         project_volumes[<project_name>][<timestamp>|"present'] =
+    #                   <array of volnames>
+    def update_volume_info(self):
+        self.project_volumes = {}
+        project_dirs = os.listdir(self.base_dir)
+        for project_name in project_dirs:
+            project_path = os.path.join(self.base_dir, project_name)
+            if not os.path.isdir(project_path):
+                continue
+
+            self.project_volumes[project_name] = {}
+            self.project_volumes[project_name]["present"] = 
os.listdir(project_path)
+
+        snap_dirs = os.listdir(self.base_snapshot_dir)
+        for snap_time in snap_dirs:
+            snap_path = os.path.join(self.base_snapshot_dir, snap_time)
+            if not os.path.isdir(snap_path):
+                continue
+
+            project_snap_dirs = os.listdir(snap_path)
+            for project_name in project_snap_dirs:
+                project_snap_path = os.path.join(snap_path, project_name)
+                if not os.path.isdir(project_snap_path):
+                    continue
+
+                if project_name not in self.project_volumes:
+                    self.project_volumes[project_name] = {}
+                self.project_volumes[project_name][snap_time] = 
os.listdir(project_snap_path)
+
+    def refresh_volumes(self):
+        base = self.ldapSupportLib.getBase()
+        ds = self.ldapSupportLib.connect()
+        projectdata = self.search_s(ds, "ou=projects," + base, 
ldap.SCOPE_ONELEVEL, "(objectclass=groupofnames)")
+        self.groupdata = self.search_s(ds, "ou=groups," + base, 
ldap.SCOPE_ONELEVEL, "(objectclass=groupofnames)")
+        hostdata = self.search_s(ds, "ou=hosts," + base, ldap.SCOPE_SUBTREE, 
"(puppetvar=instanceproject=*)", ['puppetvar', 'aRecord'])
+        self.update_volume_info()
+
+        project_hosts = self.get_hosts(hostdata)
+
+        # orphan_projects will eventually contain a list of projects that we 
have volumes for
+        #  but which no longer exist.
+        orphan_projects = self.project_volumes.keys()
+        unloved_volumes = []
+        unused_volumes = []
+        self.schedule_sync_exports = False
+
+        for project in projectdata:
+            project_name = project[1]["cn"][0]
+            desired_volumes = self.get_desired_volume_names(project)
+
+            hosts = []
+            if project_name in project_hosts:
+                hosts = project_hosts[project_name]
+                hosts = list(set(hosts))
+                hosts.sort()
+
+            present_volumes = []
+            if project_name in self.project_volumes:
+                if "present" in self.project_volumes[project_name]:
+                    present_volumes = 
self.project_volumes[project_name]["present"]
+
+            for volume_name in self.volume_names:
+                if project_name in self.manual_shares and volume_name in 
self.manual_shares[project_name]:
+                    hosts.extend(self.manual_shares[project_name][volume_name])
+
+                if project_name in self.global_shares and volume_name in 
self.global_shares[project_name]:
+                    hosts.append("*")
+
+                project_subpath = os.path.join(project_name, volume_name)
+                if project_name in orphan_projects:
+                    orphan_projects.remove(project_name)
+
+                if (volume_name in desired_volumes) and hosts:
+                    # We want this volume to exist.
+                    if volume_name not in present_volumes:
+                        # We may need to create it.
+                        rval = self.make_volume(project_name, volume_name)
+                        if rval:
+                            self.log("Created volume: " + project_name + ", " 
+ volume_name)
+                            self.schedule_sync_exports = True
+                            self.update_volume_info()
+
+                    self.update_exports(project_name, volume_name, hosts)
+                else:
+                    # All hosts have been deleted, or none have been created, 
or
+                    # we aren't supposed to have this volume to begin with.
+                    if hosts != []:
+                        # Clear all the exports.
+                        self.update_exports(project_name, volume_name, [])
+
+                    if volume_name in present_volumes:
+                        if volume_name in desired_volumes:
+                            unused_volumes.append("%s, %s" % (project_name, 
volume_name))
+                        else:
+                            unloved_volumes.append("%s, %s" % (project_name, 
volume_name))
+
+        for unused in unused_volumes:
+            self.log("Unused volume %s is possible candidate for cleanup -- no 
hosts are using it!" % unused)
+        for unloved in unloved_volumes:
+            self.log("Unwanted volume %s is possible candidate for cleanup -- 
this share was not requested." % unloved)
+        for orphan in orphan_projects:
+            self.log("Project %s no longer exists but still has allocated 
volumes." % orphan)
+
+        ds.unbind()
+        if self.schedule_sync_exports:
+            if self.dry_run:
+                print "Calling sync-exports"
+            else:
+                subprocess.call(["sudo", "/usr/local/sbin/sync-exports"])
+
+    def update_exports(self, project_name, volume_name, hosts):
+        if project_name not in self.project_volumes:
+            self.log("Unable to set exports for %s, %s because we can't find 
it." % (project_name, volume_name))
+            return False
+
+        if self.dry_run:
+            export_file_path = os.path.join("/etc/test-exports.d", 
"%s-%s.exports" % (project_name, volume_name))
+        else:
+            export_file_path = os.path.join(self.base_exports_dir, 
"%s-%s.exports" % (project_name, volume_name))
+
+        f = open(export_file_path, "w")
+
+        if not hosts:
+            f.close()
+            return
+
+        project_gid = self.get_gid_for_project(project_name)
+
+        timestamps = self.project_volumes[project_name].keys()
+        for stamp in timestamps:
+            if stamp == "present":
+                volpath = os.path.join(self.base_export_volume_dir, 
project_name)
+                yyyymmddhh = "0000000000"
+                permissions = "rw,nohide"
+            else:
+                volpath = os.path.join(self.base_export_volume_dir, 
project_name, self.export_snapshot_dir, stamp)
+                yyyymmddhh = "%s%s" % (stamp.split(".")[0], 
stamp.split(".")[1][:2])
+                permissions = "ro"
+
+            fsid = "00000000000000000-%s-%s" % (project_gid.zfill(5), 
yyyymmddhh)
+
+            if "*" in hosts:
+                # Add a special-case read-only line for this
+                exportline = "%s 
-ro,fsid=%s,subtree_check,async,no_root_squash *\n" % (
+                    volpath, permissions, fsid)
+                f.write(exportline)
+                hosts.remove("*")
+
+            exportline = "%s -%s,fsid=%s,subtree_check,async,no_root_squash 
%s\n" % (
+                volpath, permissions, fsid, " ".join(hosts))
+
+            f.write(exportline)
+        f.close()
+        self.log("Updated exports for %s, %s" % (project_name, volume_name))
+        return True
+
+    def make_volume(self, project_name, volume_name):
+        # We ensure a volume directory is unique by setting 
<base_dir>/project_name/volume_name
+        # as we know every project is unique and volumes within it also will 
be unique
+        voldir = self.base_dir + project_name + '/' + volume_name
+
+        # We need to clean up from partially-failed runs.  rmdir is pretty 
conservative; it will
+        #  only clean up empty dirs.
+        cmds1 = ['sudo', 'rmdir %s' % voldir]
+        cmds2 = ['sudo', 'mkdir -p %s' % voldir]
+
+        if self.dry_run:
+            print "Exec %s" % cmds1
+            print "Exec %s" % cmds2
+            return True
+        else:
+            subprocess.call(cmds1)
+            return subprocess.call(cmds2)
+
+    def get_gid_for_project(self, project):
+        if self.groupdata:
+            for group in self.groupdata:
+                name = group[1]["cn"][0]
+                if name == "project-%s" % project:
+                    return group[1]["gidNumber"][0]
+
+            self.log("Unable to locate group %s.  Returning 00000." % project)
+            return "00000"
+        else:
+            self.log("Without group data, I can't determine the gid!  
Returning 00000.")
+            return "00000"
+
+    def get_hosts(self, hostdata):
+        project_hosts = {}
+        if hostdata:
+            for host in hostdata:
+                host_ip = host[1]["aRecord"][0]
+                puppet_vars = host[1]["puppetvar"]
+                for puppet_var in puppet_vars:
+                    var_arr = puppet_var.split('=')
+                    if len(var_arr) == 2 and var_arr[0] == "instanceproject":
+                        project = var_arr[1]
+                        if project in project_hosts:
+                            project_hosts[project].append(host_ip)
+                        else:
+                            project_hosts[project] = [host_ip]
+                        # No need to go any further, we aren't reading other 
variables
+                        break
+        return project_hosts
+
+    # returns an array containing zero, one, or both of 'project', 'volume'
+    # Presence of these values indicate that the named shared volume should
+    # exist for a given project.
+    def get_desired_volume_names(self, projectLdapData):
+        vols = []
+        project_name = projectLdapData[1]["cn"][0]
+        if "info" in projectLdapData[1]:
+            infos = projectLdapData[1]["info"]
+            volPrefix = "use_volume="
+            for info in infos:
+                if info.startswith(volPrefix):
+                    vols.append(info.lstrip(volPrefix))
+        return vols
+
+    def search_s(self, ds, base, scope, query, attrlist=None):
+        try:
+            data = ds.search_s(base, scope, query, attrlist)
+            if not data:
+                raise ldap.NO_SUCH_OBJECT()
+            return data
+        except ldap.NO_SUCH_OBJECT:
+            sys.stderr.write("The search returned no entries.\n")
+            return None
+        except ldap.PROTOCOL_ERROR:
+            sys.stderr.write("There was an LDAP protocol error; see 
traceback.\n")
+            traceback.print_exc(file=sys.stderr)
+            return None
+        except Exception:
+            try:
+                sys.stderr.write("There was a general error, this is 
unexpected; see traceback.\n")
+                traceback.print_exc(file=sys.stderr)
+                return None
+            except Exception:
+                traceback.print_exc(file=sys.stderr)
+            return None
+
+    def log(self, logstring):
+        if self.loglevel >= INFO:
+            log = datetime.datetime.now().strftime("%m/%d/%Y - %H:%M:%S - ") + 
logstring + "\n"
+            if self.logfile:
+                lf = open(self.logfile, 'a')
+                lf.write(log)
+                lf.close()
+            else:
+                print log
+
+
+def main():
+    volume_manager = VolumeManager()
+    volume_manager.run()
+
+if __name__ == "__main__":
+    main()
diff --git a/files/upstart/manage-nfs-volumes.conf 
b/files/upstart/manage-nfs-volumes.conf
new file mode 100644
index 0000000..323fe48
--- /dev/null
+++ b/files/upstart/manage-nfs-volumes.conf
@@ -0,0 +1,8 @@
+description    "Manage shared NFS volumes for Labs projects"
+
+start on runlevel [2345]
+
+respawn
+respawn limit 10 5
+
+exec su -c "/usr/bin/python /usr/local/sbin/manage-nfs-volumes-daemon 
--logfile=/var/lib/glustermanager/manage-nfs-volumes.log" nfsmanager
diff --git a/manifests/ldap.pp b/manifests/ldap.pp
index e253419..8efb3aa 100644
--- a/manifests/ldap.pp
+++ b/manifests/ldap.pp
@@ -400,6 +400,11 @@
                        group => root,
                        mode  => 0544,
                        source => 
"puppet:///files/ldap/scripts/manage-volumes-daemon";
+               "/usr/local/sbin/manage-nfs-volumes-daemon":
+                       owner => root,
+                       group => root,
+                       mode  => 0544,
+                       source => 
"puppet:///files/ldap/scripts/manage-nfs-volumes-daemon";
                "/usr/local/sbin/manage-volumes":
                        ensure => absent;
                "/usr/local/sbin/ldapsupportlib.py":
diff --git a/manifests/openstack.pp b/manifests/openstack.pp
index c418ddf..987f0ac 100644
--- a/manifests/openstack.pp
+++ b/manifests/openstack.pp
@@ -205,6 +205,24 @@
        }
 }
 
+class openstack::project-nfs-storage-service {
+       upstart_job{ "manage-nfs-volumes":
+               install => "true";
+       }
+
+       service { "manage-nfs-volumes":
+               enable => true,
+               ensure => running,
+               require => Upstart_job["manage-nfs-volumes"];
+       }
+
+       $sudo_privs = [ 'ALL = NOPASSWD: /bin/mkdir -p /srv/*',
+                       'ALL = NOPASSWD: /bin/rmdir /srv/*',
+                       'ALL = NOPASSWD: /usr/local/sbin/sync-exports' ]
+       sudo_user { [ "nfsmanager" ]: privileges => $sudo_privs, require => 
Systemuser["nfsmanager"] }
+       systemuser { "nfsmanager": name => "nfsmanager", home => 
"/var/lib/nfsmanager", shell => "/bin/bash" }
+}
+
 class openstack::project-storage {
        include openstack::gluster-service
 
diff --git a/manifests/site.pp b/manifests/site.pp
index cad45fe..28d47a3 100644
--- a/manifests/site.pp
+++ b/manifests/site.pp
@@ -1279,7 +1279,8 @@
 
        $ganglia_aggregator = true
 
-       include standard
+       include standard,
+               openstack::project-nfs-storage-service
 
        class { "role::ldap::client::labs": ldapincludes => $ldapincludes }
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/60083
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ied54fcd71ae20377d62beb21838b436fd2235212
Gerrit-PatchSet: 5
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Andrew Bogott <[email protected]>
Gerrit-Reviewer: Andrew Bogott <[email protected]>
Gerrit-Reviewer: Ryan Lane <[email protected]>
Gerrit-Reviewer: coren <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to