Commit 42c6e214 authored by David Johnson's avatar David Johnson
Browse files

Add a CLI for FreeNAS to build Emulab clientside support.

FreeNAS is basically a web frontend to BSD-backed ZFS volumes,
filesystems, and various ways to share them (i.e., iSCSI, NFS, etc).
It stores all its config info in a sqlite DB from which it configs
the BSD system.  It uses Django (a slightly weird MVC that exports a web
interface; logic/models/views are all in python, and there is an
HTML-based template interface.

What I did was basically to wrap the model/form parts of FreeNAS's
code -- so for the commands we want to support, we actually mock up
an HTTP request, and submit it directly to the correct handler function
that the FreeNAS Django config files specify.  This allows us to leverage
all the FreeNAS error checking code and automation (i.e., deleting an
interface would delete aliases on that interface too).

usage() prints this, at present:

Supply a command set class, an operation, and the necessary arguments.

  interface       Configure network interfaces
    add  <interface> <name> [<dhcp=X> <ipv6auto=X> <options=X> ] ...
    del  <interface>
    edit <interface> [<dhcp=X> <ipv6auto=X> <name=X> <options=X> ] ...
  ist             Configure ISCSI targets (a target binds SCSI attributes
         (i.e. serial number, r/w flags, queue depth, block size) to iSCSI
         attributes (i.e., a target portal, authorized initiator network ACLs,
         iSCSI authentication info)
    add  <name> <serial> <portalgroup> <initiatorgroup> [<authtype>
         <authgroup> ] [<alias=X> <flags=X> <logical_blocksize=X>
         <queue_depth=X> <type=X> ]
    del  <name>
    edit <name> [<serial> <portalgroup> <initiatorgroup> <authtype>
         <authgroup> ]
  ist_assoc       Associate extents with targets (final "link" between storage
         and network)
    add <target> <extent>
    del <target> <extent>
  ist_authcred    Configure ISCSI target authentication credentials (i.e.,
         users)
    add  <tag> <user> <secret1> [<peeruser> <peersecret1> ]
    del  <user>
    edit <tag> <user> <secret1> [<peeruser> <peersecret1> ]
  ist_authinit    Configure ISCSI initiator authorizations by hostname or
         network
    add  <tag> <initiators> [<auth_network> <comment> ]
    del  <tag>
    edit <tag> <initiators> [<auth_network> <comment> ]
  ist_config      Configure general ISCSI parameters
    edit [<basename=X> <defaultt2r=X> <defaultt2w=X> <discoveryauthgroup=X>
         <discoveryauthmethod=X> <firstburst=X> <iotimeout=X> <maxburst=X>
         <maxconnect=X> <maxoutstandingr2t=X> <maxrecdata=X> <maxsesh=X>
         <nopinint=X> <r2t=X> ]
  ist_extent      Configure ISCSI target extents (block devs or files exported
         via ISCSI)
    add      <name> <dev> [<comment> ]
    addfile  <name> <path> <filesize> [<comment> ]
    del      <name>
    edit     <name> <dev> [<comment> ]
    editfile <name> <path> [<comment> ] [<filesize=X> ]
  ist_portal      Configure ISCSI target portals (i.e., ip:port binding to
         associate with a target)
    add  <tag> [<comment=X> ] ...
    del  <tag>
    edit <tag> [<comment=X> ] ...
  network         Configure generic network settings
    config [<domain=X> <hostname=X> <ipv4gateway=X> <ipv6gateway=X>
         <nameserver1=X> <nameserver2=X> <nameserver3=X> ]
  pool            Configure ZFS storage pools
    add <volume_name> <volume_fstype> <group_type>  ...
    del <vol_name>
    mod <volume_add> <volume_fstype> <group_type>  ...
  route           Configure static routes
    add <destination> <gateway> [<description> ]
    del <destination> [<gateway> ]
  snapshot        Create, clone, rollback ZFS snapshots of volumes or clones
    add      <snap_name>
    clone    <cs_snapshot> <cs_name>
    del      <snap_name>
    rollback <snap_name>
  vlan            Configure vlan interfaces
    add <pint> <vint> <tag> [<description> ]
    del <vint>
  volume          Configure ZFS volumes (zvols) atop pools
    add <pool_name> <zvol_name> <zvol_size> <zvol_compression>
    del <pool_name> <vol_name>
parent 000cb177
#!/usr/bin/env python
import sys
sys.path.append('/usr/local/www')
sys.path.append('/usr/local/www/freenasUI')
from freenasUI import settings
from django.core.management import setup_environ
setup_environ(settings)
import django.http
import freenasUI.urls
import freenasUI.network.models
import freenasUI.storage.models
import freenasUI.services.models
import freenasUI.freeadmin.views
class FakeHttpRequestPOST(django.http.HttpRequest):
def __init__(self,params=None):
super(FakeHttpRequestPOST,self).__init__()
self.META['HTTP_USER_AGENT'] = 'fake'
self.META['REMOTE_ADDR'] = '127.0.0.1'
self.META['REMOTE_HOST'] = 'localhost'
self.META['REQUEST_METHOD'] = 'POST'
self.FILES = django.http.MultiValueDict()
self.POST = django.http.QueryDict('',mutable=True)
self.method = 'POST'
if params != None:
for (k,v) in params.iteritems():
self.add(k,v)
pass
pass
pass
def add(self,key,value):
if self.POST.has_key(key):
self.POST.appendlist(key,value)
else:
self.POST[key] = value
pass
pass
class CommandError(Exception):
pass
class ArgError(Exception):
pass
class InstanceNotFoundError(Exception):
pass
class ValueDoesNotExistError(Exception):
pass
class ValueExistsError(Exception):
pass
class MultipleValuesError(Exception):
pass
class CommandState(object):
def __init__(self,command,args,kwargs):
self.command = command
self.args = args
self.kwargs = kwargs
self.inline_adds = []
self.inline_dels = []
pass
pass
class CommandSet(object):
def __init__(self):
self.commands = {}
pass
def _check(self,stateobj):
command = stateobj.command
args = stateobj.args
kwargs = stateobj.kwargs
if not self.commands.has_key(command):
raise CommandError("Unknown command '%s'" % command)
cdict = self.commands[command]
alen = len(args)
if alen < cdict['minArgs']:
raise ArgError("Not enough arguments")
if cdict['maxArgs'] > -1 and alen > cdict['maxArgs']:
raise ArgError("Too many arguments")
for (k,v) in kwargs.iteritems():
if not k in cdict['kwArgs']:
raise ArgError("Unknown keyword argument '%s'" % k)
pass
if cdict.has_key('reqKwArgs'):
for k in cdict['reqKwArgs']:
if not kwargs.has_key(k):
raise ArgError("Required keyword argument '%s' missing" % k)
pass
pass
#
# Only call this function after calling _check!
#
def _assembleRequest(self,stateobj):
command = stateobj.command
args = stateobj.args
kwargs = stateobj.kwargs
cdict = self.commands[command]
fakeReq = FakeHttpRequestPOST()
i = 0
model_var_prefix = self.loadattr(command,'_model_var_prefix')
for k in args:
if i >= len(cdict['argDictNames']):
# Vararg:
valist = self.handleVarArg(k,stateobj)
for (vk,vv) in valist:
fakeReq.add(vk,vv)
else:
rk = cdict['argDictNames'][i]
tk = cdict['argDictNames'][i]
if model_var_prefix != None and len(model_var_prefix) > 0:
tk = "%s_%s" % (model_var_prefix,cdict['argDictNames'][i])
# if this field needs a foreign filter lookup, do it!
if cdict.has_key('argForeignKeyModel') \
and cdict['argForeignKeyModel'].has_key(rk):
modarr = cdict['argForeignKeyModel'][rk].rsplit('.',1)
__import__(modarr[0])
model = getattr(sys.modules[modarr[0]],modarr[1])
vfilter = {}
vfilter[cdict['argForeignKeyName'][rk]] = k
values = model.objects.values().filter(**vfilter)
k = values[0]['id']
fakeReq.add(tk,k)
i += 1
pass
for (k,v) in kwargs.iteritems():
tk = k
if model_var_prefix != None and len(model_var_prefix) > 0:
tk = "%s_%s" % (model_var_prefix,k)
# if this field needs a foreign filter lookup, do it!
rk = k
if cdict.has_key('argForeignKeyModel') \
and cdict['argForeignKeyModel'].has_key(rk):
modarr = cdict['argForeignKeyModel'][rk].rsplit('.',1)
__import__(modarr[0])
model = getattr(sys.modules[modarr[0]],modarr[1])
vfilter = {}
vfilter[cdict['argForeignKeyName'][rk]] = v
values = model.objects.values().filter(**vfilter)
v = values[0]['id']
fakeReq.add(tk,v)
pass
for (k,v) in cdict['kwDefaults'].iteritems():
tk = k
if model_var_prefix != None and len(model_var_prefix) > 0:
tk = "%s_%s" % (model_var_prefix,k)
if not self.loadattr(command,'NO_AUTO_FILL') \
and not fakeReq.POST.has_key(tk):
fakeReq.add(tk,v)
pass
return fakeReq
def _getInstances(self,stateobj,filters):
#
# Only one of these, ever.
#
command = stateobj.command
model_prefix = self.loadattr(command,'_model_prefix')
modelname = self.loadattr(command,'_model')
model = eval('%s.%s' % (model_prefix,modelname))
values = model.objects.values().filter(**filters)
return values
def _run(self,stateobj):
command = stateobj.command
args = stateobj.args
kwargs = stateobj.kwargs
cdict = self.commands[command]
self._check(stateobj)
fakeReq = self._assembleRequest(stateobj)
values = []
model = self.loadattr(command,'_model')
model_prefix = self.loadattr(command,'_model_prefix')
model_var_prefix = self.loadattr(command,'_model_var_prefix')
if model != None:
filters = {}
for (k,v) in fakeReq.POST.iteritems():
#print "D: considering %s,%s" % (str(k),str(v))
if (model_var_prefix != None \
and k[len(model_var_prefix)+1:] \
in cdict['instanceFilterArgs']) \
or k in cdict['instanceFilterArgs']:
#print "D: adding %s" % (str(k),)
filters[k] = v
values = self._getInstances(stateobj,filters)
pass
#print "D: filters=%s, values=%s" % (str(filters),str(values))
mf = self.loadattr(command,'_model_form')
if cdict['type'] == 'add':
if len(values):
raise ValueExistsError("already exists (%s)" % (str(values),))
# Handle formset values. In this case, we just add a bunch of new
# formset values.
inline = self.loadattr(command,'_inline')
if inline != None:
imodel = inline['model']
iformset_prefix = inline['formset_prefix']
imodel_var_prefix = inline['model_var_prefix']
iforeign_key = inline['foreign_key']
iforeign_key_short = inline['foreign_key_short']
ifields = inline['fields']
# dump the formset params
fakeReq.add('%s-TOTAL_FORMS' % (iformset_prefix,),
len(stateobj.inline_adds))
fakeReq.add('%s-INITIAL_FORMS' % (iformset_prefix,),0)
i = 0
for iadd in stateobj.inline_adds:
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,
iforeign_key_short),'')
fakeReq.add("%s-%d-id" % (iformset_prefix,i),'')
j = 0
while j < len(inline['fields']):
fn = inline['fields'][j]
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,fn),
iadd[j])
j += 1
pass
i += 1
pass
print "generic_model_add(%s,%s,%s)" \
% (str(fakeReq),str(self._app),str(model))
ret = freenasUI.freeadmin.views.generic_model_add(fakeReq,self._app,
model,mf=mf)
print "ret = %s" % (str(ret),)
pass
elif cdict['type'] == 'edit':
if len(values) == 0:
raise ValueDoesNotExistError("instance does not exist")
elif len(values) != 1:
raise MultipleValuesError("more than one matching value to edit")
if not self.loadattr(command,'NO_AUTO_EDIT'):
print "values = %s" % (str(values),)
for (k,v) in values[0].iteritems():
if k != 'id' and not fakeReq.POST.has_key(k):
tk = k
if model_var_prefix:
tk = k[len(model_var_prefix)+1:]
if v == None and cdict['kwDefaults'].has_key(tk):
fakeReq.add(k,cdict['kwDefaults'][tk])
elif v != None:
fakeReq.add(k,v)
pass
# Handle formset values. Basically, we load the referenced values,
# filtered by our editing value's id; and if we have scheduled
# anything for deletion, we add those attrs to teh formset; if we
# don't do anything to an attr
inline = self.loadattr(command,'_inline')
if inline != None:
pk_id = values[0]['id']
imodel = inline['model']
iformset_prefix = inline['formset_prefix']
imodel_var_prefix = inline['model_var_prefix']
iforeign_key = inline['foreign_key']
iforeign_key_short = inline['foreign_key_short']
ifields = inline['fields']
# load the values
__import__(model_prefix)
im = getattr(sys.modules[model_prefix],imodel)
ivfilter = {}
ivfilter['%s_%s' % (imodel_var_prefix,iforeign_key_short)] = pk_id
ivalues = im.objects.filter(**ivfilter).values()
print "pk = %d ; ivalues = %s" % (pk_id,str(ivalues),)
# dump the formset params
fakeReq.add('%s-TOTAL_FORMS' % (iformset_prefix,),
ivalues.count() + len(stateobj.inline_adds))
fakeReq.add('%s-INITIAL_FORMS' % (iformset_prefix,),
ivalues.count())
i = 0
for iv in ivalues:
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,
iforeign_key_short),pk_id)
fakeReq.add("%s-%d-id" % (iformset_prefix,i),iv['id'])
for fn in inline['fields']:
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,fn),
iv["%s_%s" % (imodel_var_prefix,fn)])
pass
# if this one is supposed to be deleted, mark it!
for idel in stateobj.inline_dels:
should_del = True
j = 0
while j < len(inline['fields']):
fn = inline['fields'][j]
if str(iv["%s_%s" % (imodel_var_prefix,fn)]) \
!= str(idel[j]):
should_del = False
break
j += 1
pass
if should_del:
fakeReq.add("%s-%d-DELETE" % (iformset_prefix,i),
'on')
break
pass
i += 1
pass
for iadd in stateobj.inline_adds:
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,
iforeign_key_short),pk_id)
fakeReq.add("%s-%d-id" % (iformset_prefix,i),'')
j = 0
while j < len(inline['fields']):
fn = inline['fields'][j]
fakeReq.add("%s-%d-%s_%s" % (iformset_prefix,i,
imodel_var_prefix,fn),
iadd[j])
j += 1
pass
i += 1
pass
print "generic_model_edit(%s,%s,%s,%s)" \
% (str(fakeReq),str(self._app),str(model),
str(values[0]['id']))
ret = freenasUI.freeadmin.views.generic_model_edit \
(fakeReq,self._app,model,oid=values[0]['id'],mf=mf)
print "ret = %s" % (str(ret),)
pass
elif cdict['type'] == 'del':
if len(values) == 0:
raise ValueDoesNotExistError("instance does not exist")
for v in values:
print "generic_model_delete(%s,%s,%s,%s)" \
% (str(fakeReq),str(self._app),str(model),
str(v['id']))
ret = freenasUI.freeadmin.views.generic_model_delete \
(fakeReq,self._app,model,oid=v['id'])
print "ret = %s" % (str(ret),)
pass
elif cdict['type'] == 'custom':
modfunc = cdict['function'].rsplit('.',1)
if len(modfunc) == 2:
__import__(modfunc[0])
mod = sys.modules[modfunc[0]]
func = getattr(mod,modfunc[1])
else:
func = eval(modfunc[0])
args = self.assembleArgs(fakeReq,values,stateobj)
print 'DEBUG: calling custom %s on %s' % (str(func),str(args))
ret = func(*args)
print 'DEBUG: custom ret: %s' % (str(ret),)
pass
pass
def run(self,command,args,kwargs):
stateobj = CommandState(command,args,kwargs)
self._run(stateobj)
def loadattr(self,command,attr):
if self.commands[command].has_key(attr):
return self.commands[command][attr]
else:
return getattr(self,attr,None)
pass
class Network(CommandSet):
_help = 'Configure generic network settings'
def __init__(self):
super(Network,self).__init__()
self._app = 'network'
self._model_var_prefix = 'gc'
self._model_prefix = 'freenasUI.network.models'
self._model = 'GlobalConfiguration'
self.commands['config'] = \
dict({ 'minArgs' : 0, 'maxArgs' : 0,
'argDictNames' : [],
'kwArgs' : [ 'hostname','domain','ipv4gateway','ipv6gateway',
'nameserver1','nameserver2','nameserver3' ],
'kwDefaults' : {},
'instanceFilterArgs' : [],
'type' : 'edit' })
pass
pass
class Interface(CommandSet):
_help = 'Configure network interfaces'
def __init__(self):
super(Interface,self).__init__()
self._app = 'network'
self._model_var_prefix = 'int'
self._model_prefix = 'freenasUI.network.models'
self._model = 'Interfaces'
self._inline = { 'model' : 'Alias',
'formset_prefix' : 'alias_set',
'model_var_prefix' : 'alias',
'foreign_key' : 'interface_id',
'foreign_key_short' : 'interface',
'fields' : [ 'v4address','v4netmaskbit',
'v6address','v6netmaskbit' ] }
self.commands['add'] = \
dict({ 'minArgs' : 2, 'maxArgs' : -1,
'argDictNames' : [ 'interface','name' ],
'varArgs' : True,
'kwArgs' : [ 'dhcp','ipv6auto','options' ],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'interface' ],
'type' : 'add' })
self.commands['edit'] = \
dict({ 'minArgs' : 1, 'maxArgs' : -1,
'argDictNames' : [ 'interface' ],
'varArgs' : True,
'kwArgs' : [ 'name','dhcp','ipv6auto','options' ],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'interface' ],
'type' : 'edit' })
self.commands['del'] = \
dict({ 'minArgs' : 1, 'maxArgs' : 1,
'argDictNames' : [ 'interface' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'interface' ],
'type' : 'del' })
pass
def handleVarArg(self,arg,stateobj):
command = stateobj.command
retval = []
v6 = False
alias_add = False
alias_del = False
iface = False
if arg.startswith('+'):
# adding an alias
arg = arg[1:]
alias_add = True
elif arg.startswith('-'):
arg = arg[1:]
alias_del = True
else:
iface = True
if arg.find('/') < 1:
raise ArgError("bad address/netmaskbit '%s'" % (arg,))
[ ip,netmaskbit ] = arg.rsplit('/',1)
netmaskbit = int(netmaskbit)
if ip.find(':') > -1:
v6 = True
# now actually return a value, OR edit our inline_(add|del) lists
# in the stateobj to populate the formset
if iface:
model_var_prefix = self.loadattr(command,'_model_var_prefix')
if v6:
retval.append([ "%s_ipv6address" % (model_var_prefix,),ip ])
retval.append([ "%s_v6netmaskbit" % (model_var_prefix,),
netmaskbit ])
else:
retval.append([ "%s_ipv4address" % (model_var_prefix,),ip ])
retval.append([ "%s_v4netmaskbit" % (model_var_prefix,),
netmaskbit ])
elif alias_add:
if v6:
stateobj.inline_adds.append([ '','',ip,netmaskbit ])
else:
stateobj.inline_adds.append([ ip,netmaskbit,'','' ])
elif alias_del:
if v6:
stateobj.inline_dels.append([ '','',ip,netmaskbit ])
else:
stateobj.inline_dels.append([ ip,netmaskbit,'','' ])
return retval
pass
class Route(CommandSet):
_help = 'Configure static routes'
def __init__(self):
super(Route,self).__init__()
self._app = 'network'
self._model_var_prefix = 'sr'
self._model_prefix = 'freenasUI.network.models'
self._model = 'StaticRoute'
self.commands['add'] = \
dict({ 'minArgs' : 2, 'maxArgs' : 3,
'argDictNames' : [ 'destination','gateway','description' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'destination','gateway' ],
'type' : 'add' })
self.commands['del'] = \
dict({ 'minArgs' : 1, 'maxArgs' : 2,
'argDictNames' : [ 'destination','gateway' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'destination','gateway' ],
'type' : 'del' })
pass
pass
class Vlan(CommandSet):
_help = 'Configure vlan interfaces'
def __init__(self):
super(Vlan,self).__init__()
self._app = 'network'
self._model_var_prefix = 'vlan'
self._model_prefix = 'freenasUI.network.models'
self._model = 'VLAN'
self.commands['add'] = \
dict({ 'minArgs' : 3, 'maxArgs' : 4,
'argDictNames' : [ 'pint','vint','tag','description' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'pint','vint','tag' ],
'type' : 'add' })
self.commands['del'] = \
dict({ 'minArgs' : 1, 'maxArgs' : 1,
'argDictNames' : [ 'vint' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'pint','vint','tag' ],
'type' : 'del' })
pass
pass
class Pool(CommandSet):
_help = 'Configure ZFS storage pools'
def __init__(self):
super(Pool,self).__init__()
self.commands['add'] = \
dict({ 'minArgs' : 3, 'maxArgs' : -1,
'argDictNames' : [ 'volume_name','volume_fstype','group_type' ],
'varArgs' : True,
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [],
'type' : 'custom',
'function' : 'freenasUI.storage.views.wizard' })
self.commands['mod'] = \
dict({ 'minArgs' : 3, 'maxArgs' : -1,
'argDictNames' : [ 'volume_add','volume_fstype','group_type' ],
'varArgs' : True,
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [],
'type' : 'custom',
'function' : 'freenasUI.storage.views.wizard' })
self.commands['del'] = \
dict({ 'minArgs' : 1, 'maxArgs' : 1,
'argDictNames' : [ 'vol_name' ],
'kwArgs' : [],
'kwDefaults' : {},
'instanceFilterArgs' : [ 'vol_name' ],
'_model_prefix' : 'freenasUI.storage.models',
'_model' : 'Volume',
'type' : 'custom',