text: # Copyright (c) 2015 The New Mexico Consortium
  # 
  # {{{NMC-LICENSE
  #
  # All rights reserved.
  #
  # Redistribution and use in source and binary forms, with or without
  # modification, are permitted provided that the following conditions
  # are met:
  #
  # 1. Redistributions of source code must retain the above copyright
  # notice, this list of conditions and the following disclaimer.
  #
  # 2. Redistributions in binary form must reproduce the above copyright
  # notice, this list of conditions and the following disclaimer in the
  # documentation and/or other materials provided with the distribution.
  #
  # 3. Neither the name of the copyright holder nor the names of its
  # contributors may be used to endorse or promote products derived from
  # this software without specific prior written permission.
  #
  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
  # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
  # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
  # DAMAGE.
  #
  # }}}
  
  import re, subprocess
  from nmc_probe.log import Log
  
  class Assert:
      @classmethod
      def typeIs(cls, arg, tp):
          if not type(arg) is tp:
              message = 'Type %s required, received %s: %s' % (tp.__name__,  type(arg).__name__, repr(arg))
              Log.error(message)
              raise TypeError(message)
              
      @classmethod
      def typeIsNamespace(cls, arg):
          return cls.typeIs(arg, dict)
  
  class CommandError(Exception):
      """Exception raised for errors when running a command.
  
      Attributes:
          exitcode -- the exitcode
          output -- array of lines of output from the command
          command -- array of arguments for the command
      """
      def __init__(self, output, exitcode, command):
          self.exitcode = exitcode
          self.output = output
          self.command = command
  
      def __str__(self):
          return repr(self.output)
  
  class ZFS:
      def __init__(self):
          self.zfs = '/sbin/zfs'
  
      def getCount(self):
          if getattr(self, '_count', None) is None:
              self._count = 0
          else:
              self._count = self._count + 1
  
          return self._count
  
      @property
      def all(self):
          if getattr(self, '_all', None) is None:
              self._all = self.list('all')
          return self._all
  
      @property
      def filesystems(self):
          if getattr(self, '_filesystems', None) is None:
              self._filesystems = self.list('filesystem')
          return self._filesystems
  
      @property
      def bookmarks(self):
          if getattr(self, '_bookmarks', None) is None:
              self._bookmarks = self.list('bookmark')
          return self._bookmarks
  
      @property
      def volumes(self):
          if getattr(self, '_volumes', None) is None:
              self._volumes = self.list('volume')
          return self._volumes
  
      @property
      def snapshots(self):
          if getattr(self, '_snapshots', None) is None:
              self._snapshots = self.list('snap')
          return self._snapshots
  
      @property
      def attributes(self):
          if getattr(self, '_attributes', None) is None:
              self._attributes = self.getAttributes()
          return self._attributes
  
      def getAttributes(self):
          attributes = {}
          output = self.run(['get', 'all'])
              
          for line in output:
              # Break into (name, used, avail, refer, mountpoint)
              components = line.split()
              if len(components) == 4:
                  name = components[0]
                  property = components[1]
                  value = components[2]
                  source = components[3]
                      
                  if not attributes.has_key(name):
                      attributes[name] = {}
  
                  attributes[name][property] =  {'value': value, 'source': source}
  
          return attributes
  
      def run(self, args):
          '''Run a zfs command'''
          cmd = args
          cmd.insert(0, self.zfs)
          output = ''
          try:
              Log.info(' '.join(cmd))
              output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).split('\n')
          except subprocess.CalledProcessError, e:
              raise ValueError(str(e.output))
  
          # Remove the first list of output
          output.pop(0)
  
          return output
  
      def set(self, name, property, value):
          self.run(['set', '%s=%s' % (property, value), dataset])
  
      def udevd_settle_down(self):
          '''Wait for udevd to create /dev/zvol devices'''
          udevadm_cmd = '/usr/sbin/udevadm'
          subprocess.check_call([udevadm_cmd, 'settle'])
  
      def create(self, params):
          '''zfs create
          
          params -- { name: name,
                      properties:   {property: value, p2: v2, ...},
                      volume:       {size: size, sparse: true|false}
                      createParent: true or false} }
          '''
          # If no arguments provided, return immediately
          if params is None:
              raise ValueError('zfs create function received no parameters')
  
          command = ['create']
  
          name         = params.get('name', None)
          properties   = params.get('properties', None)
          volume       = params.get('volume', None)
          createParent = params.get('createParent', None)
  
          if not name:
              raise ValueError('zfs create function needs a name parameter')
  
          if createParent:
              command.append('-p')
  
          if not properties is None:
              for property, value in properties.iteritems():
                  command.append('-o')
                  command.append('%s=%s' % (property, value))
  
          if not volume is None:
              size = volume.get('size', None)
              sparse = volume.get('sparse', None)
  
              if not size:
                  raise ValueError('Volumes must have a size attribute')
  
              command.append('-V')
              command.append(size)
  
              if sparse and sparse != False:
                  command.append('-s')
  
          command.append(name)
  
          # Flag is set if the dataset is created
          created = None
          if self.all.has_key(name):
              if volume:
                  Log.info('%s already exists. Not creating volume' % name)
              else:
                  Log.info('%s already exists. Not creating filesystem' % name)
          else:
              self.run(command)
              created = True
  
          return created
  
      def destroy(self, params):
          # Verify that parameters were received
          if params is None:
              raise ValueError('zfs destroy function received no parameters')
  
          # Get the name
          name = params.get('dst', None)
  
          if not name:
              raise ValueError('zfs destroy function needs a name parameter')
  
          if self.all.has_key(name):
              command = ['destroy', name]
              self.run(command)
          else:
              Log.info('%s does not exist. Cannot destroy' % name)
  
      def snapshot(self, params):
          if params is None:
              raise ValueError('zfs snapshot function received no parameters')
  
          name = params.get('name', None)
  
          if name is None:
              raise ValueError('zfs snapshot function needs a name parameter')
  
          if not self.all.has_key(name):
              command = ['snapshot', name]
              self.run(command)
          else:
              Log.info('%s already exists. Not creating snapshot' % name)
  
      def clone(self, params):
          if params is None:
              raise ValueError('zfs clone function received no parameters')
  
          command = ['clone']
  
          snapshot     = params.get('src', None)
          properties   = params.get('properties', None)
          dest         = params.get('dst', None)
          createParent = params.get('createParent', None)
  
          if not snapshot:
              raise ValueError('zfs clone function needs a snapshot parameter')
  
          if not dest:
              raise ValueError('zfs clone function needs a dest parameter')
  
          if createParent:
              command.append('-p')
  
          if not properties is None:
              for property, value in properties.iteritems():
                  command.append('-o')
                  command.append('%s=%s' % (property, value))
  
          command.append(snapshot)
          command.append(dest)
  
          if self.all.has_key(snapshot):
              if self.all.has_key(dest):
                  Log.info('%s already exists. Not creating clone' % dest)
              else:
                  self.run(command)
          else:
              raise ValueError('Snapshot %s does not exist, clone %s cannot be created' % (snapshot, dest))
          
      def promote(self, clone):
          '''Promote a clone'''
          command = ['promote', clone]
          self.run(command)
  
      def list(self, opts = None):
          args = ['list']
  
          if opts:
              args.append('-t')
              args.append(opts)
  
          # Get list
          output = self.run(args)
  
          # Empty hash of datasets
          datasets = {}
  
          # Process each line of output
          for line in output:
              # Break into (name, used, avail, refer, mountpoint)
              components = line.split()
              if len(components) == 5:
                  datasets[components[0]] = {
                      'available':  components[1],
                      'refer':      components[2],
                      'used':       components[3],
                      'mountpoint': components[4]
                  }
  
          return datasets
  