Difference between revisions of "ACF mvc.lua example"

From Alpine Linux
Jump to: navigation, search
(mvc on_load & exec special functions)
(Add explanation of how the web app and acf-cli use the dispatch method with application-specific controllers)
 
(14 intermediate revisions by 5 users not shown)
Line 4: Line 4:
  
  
For this example, we will assume you have root access on the linux box you are running on (preferably an alpine box!)
+
For this example, we will assume you have root access on the linux box you are running on (preferably an Alpine Linux box!)
  
 
== Get the mvc.lua module ==  
 
== Get the mvc.lua module ==  
  
svn "export" the mvc.lua module. Export will grab a copy without all the extra hidden "version control" stuff
+
Get the mvc.lua module from the git repository.
  
  svn export svn://svn.alpinelinux.org/acf/core/trunk/www/cgi-bin/mvc.lua
+
  wget http://git.alpinelinux.org/cgit/acf-core/plain/lua/mvc.lua
  
 
== Create a model and controller ==
 
== Create a model and controller ==
  
Create a file '''hostname-model.lua''', defining the module functions to set and read the hostname.  We return a table for each function including the value, error message and type of the value ("String" in the case of the hostname).
+
Create a file '''hostname-controller.lua''', defining the functions that an "end user" could run. We will only create one action, edithostname, which will be used to read and update the hostname. Since the action makes changes to the system, it naturally takes the form of a 'form':
 +
 
 +
=== hostname-controller.lua ===
 +
-- Controller for editing hostname
 +
local mymodule = {}
 +
 +
mymodule.edithostname = function (self)
 +
        return self.handle_form(self, self.model.get_hostname, self.model.set_hostname, self.clientdata, "Update", "Edit Hostname", "Hostname Updated")
 +
end                                 
 +
 +
return mymodule
 +
 
 +
Create a file '''hostname-model.lua''', defining the model functions to get and set the hostname.  We return a cfe table for each function including the form with the one entry for hostname:
  
 
=== hostname-model.lua ===
 
=== hostname-model.lua ===
 
  -- Model functions for retrieving / setting the hostname
 
  -- Model functions for retrieving / setting the hostname
  module ( ..., package.seeall )
+
  local mymodule = {}
 
   
 
   
  -- All functions return a table with
+
  -- Create a cfe defining the form for editing the hostname and containing the current value
  -- A value, the type of the value, and a message if there was an error
+
  mymodule.get_hostname = function(self, clientdata)
 +
        local retval = cfe({ type="group", value={}, label="Hostname" })
 
   
 
   
  local hosttype={ type="string" }
+
        -- Warning - io.popen has security risks, never pass user data to io.popen
 +
        local f = io.popen ("/bin/hostname")
 +
        local n = f:read("*a") or "none"
 +
        f:close()
 +
        n=string.gsub(n, "\n$", "")
 +
   
 +
        retval.value.hostname = cfe({ value=n, label="Hostname" })
 +
 +
        return retval
 +
end
 
   
 
   
  update= function ( name )
+
  -- Set the hostname from the value contained in the cfe created by get_hostname
         -- Check to make sure the name is valid
+
mymodule.set_hostname = function(self, hostnameform, action)
 +
         local success = true
 
   
 
   
         if (name == nil) then
+
        -- Check to make sure the name is valid
                 hosttype.msg = "Hostname cannot be nil"
+
         if (hostnameform.value.hostname.value == "") then
         elseif (#name > 16) then
+
                 success = false
                 hosttype.msg = "Hostname must be less than 16 chars"
+
                hostnameform.value.hostname.errtxt = "Hostname must not be blank"
         elseif (string.find(name, "[^%w%_%-]")) then
+
         elseif (#hostnameform.value.hostname.value > 16) then
                 hosttype.msg = "Hostname can contain alphanumerics only"
+
                 success = false
 +
                hostnameform.value.hostname.errtxt = "Hostname must be 16 characters or less"
 +
         elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then
 +
                 success = false
 +
                hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only"
 
         end
 
         end
 
   
 
   
         -- If it is, set the hostname
+
         -- If it is valid, set the hostname
         if (hosttype.msg == nil ) then
+
         if ( success ) then
 
                 local f = io.open("/etc/hostname", "w")
 
                 local f = io.open("/etc/hostname", "w")
 
                 if f then
 
                 if f then
                         f:write(name .. "\n")
+
                         f:write(hostnameform.value.hostname.value .. "\n")
 
                         f:close()
 
                         f:close()
 
                 end
 
                 end
 +
                -- Warning - io.popen has security risks, never pass user data to io.popen
 
                 f = io.popen ("/bin/hostname -F /etc/hostname")
 
                 f = io.popen ("/bin/hostname -F /etc/hostname")
 
                 f:close()
 
                 f:close()
                return read()
 
        -- Otherwise, return the error message
 
 
         else
 
         else
                 hosttype.value = name
+
                 hostnameform.errtxt = "Failed to set hostname"
                return hosttype
 
 
         end
 
         end
end
 
 
   
 
   
read= function ()
+
         return hostnameform
        local f = io.popen ("/bin/hostname")
 
        local n = f:read("*a") or "none"
 
        f:close()
 
        n=string.gsub(n, "\n$", "")
 
        hosttype.value = n
 
         return (hosttype)
 
 
  end
 
  end
 
 
 
 
 
 
Create a file '''hostname-controller.lua''', defining the functions that an "end user" could run.  We define '''C'''reate '''R'''ead '''U'''pdate '''D'''elete as standard actions:
 
 
=== hostname-controller.lua ===
 
-- hostname controller code
 
 
   
 
   
  module ( ... , package.seeall )
+
  return mymodule
 
 
create = function (self )     
 
        return self.model.update(self.clientdata.hostname)
 
end                                 
 
   
 
read = function (self)
 
        return self.model.read()
 
end                           
 
 
update =  create
 
   
 
delete = function (self )
 
        self.clientdata.hostname=""                     
 
        return self.worker":create()     
 
end
 
  
 
== Optionally test the model code (without mvc.lua)  ==
 
== Optionally test the model code (without mvc.lua)  ==
Line 95: Line 87:
  
 
=== test.lua ===
 
=== test.lua ===
  m=require("hostname-model")              
+
require("mvc") -- Needed for cfe function definition
 +
  m=require("hostname-model")
 
   
 
   
  print(m.update(arg[1]).msg)
+
  local form = m.get_hostname()
  print(m.read().value)
+
form.value.hostname.value = arg[1] or ""
 +
form = m.set_hostname(nil, form)
 +
   
 +
if form.errtxt then
 +
        print("FAILED: "..form.value.hostname.errtxt or form.errtxt)
 +
end
 +
form = m.get_hostname()
 +
print(form.value.hostname.value)
  
 
You can then test this with:
 
You can then test this with:
  
 
  #lua test.lua "Alpine"
 
  #lua test.lua "Alpine"
  nil
 
 
   Alpine
 
   Alpine
  
 
  #lua test.lua "Invalid Name"
 
  #lua test.lua "Invalid Name"
   Hostname can contain alphanumerics only
+
   FAILED: Hostname can contain alphanumerics only
 
   Alpine
 
   Alpine
  
== Make an MVC based application ==
+
== Add the package to the ACF framework ==
  
To make the model and and controller work within the mvc.lua framework, we must do serveral things.   
+
To make the model and controller work within the ACF mvc.lua framework, we must do several things.   
  
1. Create a configuration file.  We'll call the application ''helloworld'', so edit helloworld.conf and add:
+
1. Install the ACF core package:
  
  appdir=helloworld/app/
+
  apk add acf-core
  
2. Move the model and controller to the helloworld app directory:
+
Optionally, you can add the entire web-based ACF framework:
  
  mkdir -p helloworld/app
+
  setup-acf
mv helloworld-*.lua helloworld/app
 
  
3. Create an application level controller in the helloworld/app directory, named '''helloworld/app/app-controller.lua'''
+
2. Modify the ACF configuration file to look in /etc/acf/app/ for additional packages. Edit the /etc/acf/acf.conf file to add the ''/etc/acf/app/'' directory to the ''appdir'' comma-separated list:
  
  module ( ..., package.seeall)
+
  appdir=/etc/acf/app/,/usr/share/acf/app/
-- application specific functions will go here
 
  
Nothing else needs to go in this controller for now.
+
3. Move the model and controller to the new package directory. We will call the package "test":
  
4. Create a dispatch wrapper program, named '''helloworld.lua''' in the current directory:
+
mkdir -p /etc/acf/app/test
 +
mv hostname-*.lua /etc/acf/app/test
  
-- Simple CLI based mvc application
+
4. Test the new package using the acf-cli application:
 
-- this is to get around having to store
 
-- the config file in /etc/helloworld/helloworld.conf
 
ENV={}
 
ENV.HOME="."
 
 
-- load the module
 
require("mvc")
 
 
-- create an new "mvc object"
 
MVC=mvc:new()
 
 
-- load the config file so we can find the appdir
 
MVC:read_config("helloworld")
 
 
-- create an application container
 
APP=MVC:new("app")
 
 
 
-- dispatch the request
 
APP.clientdata.hostname=arg[2]
 
APP:dispatch( "", "hostname", (arg[1] or ""))
 
  
  -- destroy the mvc objects
+
  # acf-cli /test/hostname/edithostname
  APP:destroy()
+
result = {}
  MVC:destroy()
+
result["label"] = "Edit Hostname"
 +
result["option"] = "Edit"
 +
result["type"] = "form"
 +
result["value"] = {}
 +
result["value"]["hostname"] = {}
 +
result["value"]["hostname"]["label"] = "Hostname"
 +
result["value"]["hostname"]["type"] = "text"
 +
result["value"]["hostname"]["value"] = "Alpine"
 +
# acf-cli /test/hostname/edithostname hostname=test submit=true
 +
result = {}
 +
result["descr"] = "Hostname Updated"
 +
result["label"] = "Edit Hostname"
 +
result["option"] = "Edit"
 +
result["type"] = "form"
 +
result["value"] = {}
 +
result["value"]["hostname"] = {}
 +
result["value"]["hostname"]["label"] = "Hostname"
 +
  result["value"]["hostname"]["type"] = "text"
 +
  result["value"]["hostname"]["value"] = "test"
  
This application loads the "mvc.lua" framework, creates an mvc "object" named "MVC", then reads the helloworld.conf file to find out where the app dir is (helloworld/app/).  It then loads the '''app-controller.lua''' into a new "application level" object named '''APP'''. Finally, it sets the clientdata and dispatches the hostname-controller/model pair.
+
Note that the action string passed to acf-cli is of the form "/prefix/controller/action". The prefix is the path within /etc/acf/app where the controller may be found, so "/test/" for our case. The controller is determined from the controller file name, so "hostname" for our case. And, the action corresponds to the function within the controller to call, so "edithostname" for our case. Also note that, in the two examples above, the first case reads the existing hostname and the second case updates it. The output of the acf-cli application is a serialized version of the cfe form, which is good for testing but not too useful in real life.
  
5.  Test the application:
+
== Enable the package in the ACF web interface ==
# hostname
 
Alpine
 
# lua helloworld.lua no-such-function foo
 
The following unhandled application error occured:
 
 
controller: "hostname" does not have a "no-such-function" action.
 
# hostname
 
Alpine
 
  
# hostname
+
1. Add the web-based ACF framework:
Alpine
 
# lua helloworld.lua update Alline
 
Your controller and application did not specify a view resolver.
 
The MVC framework has no view available. sorry.
 
# hostname
 
Alline
 
  
 +
setup-acf
  
Note in the second case the hostname ''was'' changed, although the application does not know how to report success.
+
2. Configure all users to have access to the new hostname action. Edit "/etc/acf/app/test/hostname.roles" file and add GUEST permission for the edithostname action:
  
 +
echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles
  
== Create a view resolver and view formatter ==
+
The new action should now be visible by browsing to ''https://IP-of-host/cgi-bin/acf/test/hostname/edithostname''. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname.
  
The view resolver is a function that returns a function that processes the view. the second function (the one that is returned) receives input and processes the output.
+
3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item:
  
We will build a very simple view resolver and view processor for our application. Add this to the end of '''helloworld/app/hostname-controller.lua'''
+
echo "Test Hostname Edit edithostname" > /etc/acf/app/test/hostname.menu
  
 +
You will need to log off from the ACF interface (or delete the session cookie) before the new menu item will be visible.
  
local private = {}
+
== Make an MVC based application ==
+
You have two options for creating an MVC based application of your own.
private.view = function (f)
 
        if (f.msg) then
 
                print( (f.value or "") .. " is not a valid hostname ")
 
        else
 
                print ("Hostname is currently " .. f.value )
 
        end
 
end
 
 
view_resolver = function (self)
 
        return private.view
 
end
 
  
 +
1. Use the ''dispatch'' function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format ''/prefix/controller/action'' and the input values are passed in as a clientinfo table. Output is determined by the view.
  
Now we can test:
+
2. Use the ''new'' function to load the desired controller and then directly call the controller actions or model functions. Using the controller actions requires use of the clientdata structure to pass parameters. For calling the model functions, the application will call the get function to retrieve the form cfe, fill in the desired input values into the cfe, and then call the set function to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results.
  
  # lua helloworld.lua update "1 2 3"
+
=== Use the Dispatch method ===
    1 2 3 is not a valid hostname  
+
==== test_dispatch ====
  # lua helloworld.lua update       
+
  #!/usr/bin/lua
    is not a valid hostname
+
-- Simple CLI based mvc application
  # lua helloworld.lua update Alpine
+
  Hostname is currently Alpine
+
-- load the mvc module
 +
mvc = require("acf.mvc")
 +
 +
-- create an new "mvc object"
 +
MVC=mvc:new()
 +
 +
-- load the config file so we can find the appdir
 +
MVC:read_config("acf")
 +
 +
-- dispatch the request
 +
local clientdata = {hostname=arg[1], viewtype=arg[2], submit="Update"}
 +
MVC:dispatch("/test/", "hostname", "edithostname", clientdata)
 +
   
 +
-- destroy the mvc object
 +
  MVC:destroy()
  
 +
Test the application. As can be seen in the code above, the first argument is the new hostname and the second is a viewtype.
  
But we have two problems:
+
alpine:~# chmod 755 test_dispatch
 +
alpine:~# ./test_dispatch test
 +
test:~# ./test_dispatch alpine
 +
alpine:~#
  
1. We now have to make a view resolver and view function for every controller.  If we add a ''date'' setting controller, we'll have to make a view resolver and view function for it, and so on.
+
Note that the application functions, but there is no output. This is because there is no viewtype supplied. There is built-in support for these standard viewtypes (not all apply): html, json, stream, serialized
  
2. Perhaps more importantly, view_resolver is now an "action" in our applictionRecall that invalid actions are captured, but try this:
+
alpine:~# ./test_dispatch test serialized
 +
result = {}
 +
result["descr"] = "Hostname Updated"
 +
result["label"] = "Edit Hostname"
 +
result["option"] = "Update"
 +
result["type"] = "form"
 +
result["value"] = {}
 +
result["value"]["hostname"] = {}
 +
result["value"]["hostname"]["label"] = "Hostname"
 +
result["value"]["hostname"]["type"] = "text"
 +
result["value"]["hostname"]["value"] = "test"
 +
test:~# ./test_dispatch alpine json
 +
  {"type":"form","label":"Edit Hostname","value":{"hostname":{"value":"alpine","type":"text","label":"Hostname"}},"option":"Update","descr":"Hostname Updated"}
 +
alpine:~#
  
  # lua helloworld.lua no_such_action Alpine
+
You can also use a custom viewtype and/or view to customize the output. This relies on haserl to parse the view file, and haserl lua functions are only available when launched from haserl scripts (it would be nice if haserl were available as a standalone lua library). Haserl scripts expect to be launched by a web browser to handle CGI data, so passing input is trickier. I'm sure there is a better way to do this, but this is just an example:
    The following unhandled application error occured:
 
 
 
    controller: "hostname" does not have a "no_such_action" action.
 
 
 
  # lua helloworld.lua view_resolver Alpine
 
    The following unhandled application error occured:
 
 
 
    ./helloworld/app/hostname-controller.lua:27: attempt to index local 'f' (a   function value)
 
    stack traceback:
 
        ./helloworld/app/hostname-controller.lua:27: in function 'viewfunc'
 
        ./mvc.lua:139: in function <./mvc.lua:90>
 
        [C]: in function 'xpcall'
 
        ./mvc.lua:90: in function 'dispatch'
 
        helloworld.lua:23: in main chunk
 
        [C]: ?
 
  
 +
==== test_haserl ====
 +
#!/usr/bin/haserl-lua5.2 --shell=lua
 +
<%
 +
-- Simple CLI based mvc application
 +
 +
-- load the mvc module
 +
mvc = require("acf.mvc")
 +
 +
-- create an new "mvc object"
 +
MVC=mvc:new()
 +
 +
-- load the config file so we can find the appdir
 +
MVC:read_config("acf")
 +
 +
-- dispatch the request
 +
local clientdata = {hostname=FORM.hostname, viewtype=FORM.viewtype, submit="Update"}
 +
MVC:dispatch("/test/", "hostname", "edithostname", clientdata)
 +
 +
-- destroy the mvc object
 +
MVC:destroy()
 +
%>
  
Because view_resolver is an action in the worker table, the '''mvc.lua''' runs it; but it returns a function, not a table, and causes an unhandled exception.
+
==== /etc/acf/app/test/hostname-edithostname-text.lsp ====
 +
<%
 +
local data, viewlibrary, page_info, session = ...
 +
 +
if data.errtxt then
 +
        print(data:print_errtxt())
 +
else
 +
        print(data.descr)
 +
end
 +
%>
  
== Move the view resolver to the application level ==
+
Test the application
  
The solution to both problems is to move the view resolver and the view function out of the controller's worker table, into the next higher level, in this case, the application's worker table:
+
test:~# chmod 755 test_haserl
 +
test:~# QUERY_STRING='hostname=alpine&viewtype=text' REQUEST_METHOD=GET ./test_haserl
 +
Hostname Updated
 +
alpine:~# QUERY_STRING='hostname=asdfasdfasdfasdfasdf&viewtype=text' REQUEST_METHOD=GET ./test_haserl
 +
Failed to set hostname
 +
hostname: Hostname must be 16 characters or less
 +
alpine:~#
  
 +
You can further customize the application by creating your own application-specific controller. This is how the web interface and the acf-cli application are written. In both cases, a simple application is written to load not just an mvc:new() reference, but an application-specific controller to wrap the mvc:new() object. The dispatch function is then called on the application-specific controller object, which can override any function in mvc.lua to add a customized implementation. For example, both applications override the handle_clientdata function to customize the way input data is provided in the clientdata structure, and the web interface-specific controller contains a lot of code to handle features such as menus, templates, skins, authentication, redirection, ... The best way to understand this is to just read the code.
  
1. Delete the private.view and view_resolver functions from '''helloworld/app/hostname-controller.lua'''
+
* Web application
 +
** Application - [http://git.alpinelinux.org/cgit/acf/acf-core/tree/www/cgi-bin/acf acf]
 +
** Controller - [http://git.alpinelinux.org/cgit/acf/acf-core/tree/app/acf_www-controller.lua acf_www-controller.lua]
 +
* acf-cli
 +
** Application - [http://git.alpinelinux.org/cgit/acf/acf-core/tree/bin/acf-cli acf-cli]
 +
** Controller - [http://git.alpinelinux.org/cgit/acf/acf-core/tree/app/acf_cli-controller.lua acf_cli-controller.lua]
  
2. Add the following to '''helloworld/app/app-controller.lua'''
+
=== Use the New method ===
 
+
==== test_new ====
  local private = {}
+
#!/usr/bin/lua
 +
-- Simple CLI based mvc application
 +
 +
-- load the mvc module
 +
mvc = require("acf.mvc")
 +
 +
-- create an new "mvc object"
 +
MVC=mvc:new()
 +
 +
-- load the config file so we can find the appdir
 +
MVC:read_config("acf")
 +
 +
-- load the hostname controller
 +
HOSTNAME=MVC:new("/test/hostname")
 +
 +
  local hostname
 +
 +
-- METHOD 1 - controller action
 +
HOSTNAME.clientdata = {hostname=arg[1], submit="Update"}
 +
hostname = HOSTNAME:edithostname()
 +
 +
-- METHOD 2 - model functions
 +
hostname = HOSTNAME.model:get_hostname()
 +
hostname.value.hostname.value = arg[1]
 +
hostname = HOSTNAME.model:set_hostname(hostname)
 
   
 
   
  private.view = function ( controller, action, viewtable )
+
  if hostname.errtxt then
         io.write(string.format("Controller: %s  Action: %s\n",
+
         print(hostname:print_errtxt())
                controller or "", action or ""))
+
else
        io.write ("Returned a table with the following values:\n")
+
         print(hostname.descr or "Hostname Updated")
 
 
         for k,v in pairs(viewtable) do
 
                io.write(string.format("%s\t%s\n", k, v))
 
        end
 
 
  end
 
  end
 
   
 
   
  view_resolver = function (self)
+
  HOSTNAME:destroy()
        return function (viewtable)
+
MVC:destroy()
                return private.view (self.conf.controller, self.conf.action, viewtable)
 
        end
 
end
 
 
 
  
This creates a more "generic" view, but one that will work for any controller - not just '''hostname'''.
+
Once you have a hostname controller object, you can access its actions, passing data through clientdata, or you can directly access the get/set functions of the model. Test the app:
  
 +
test:~# chmod 755 test_new
 +
test:~# ./test_new alpine
 +
Hostname Updated
 +
alpine:~# ./test_new asdfasdfasdfasdfasdf
 +
Failed to set hostname
 +
hostname: Hostname must be 16 characters or less
 +
alpine:~#
  
Now things work as they should:
 
  
# lua helloworld.lua update "one two"
+
{{Obsolete}}
  Controller: hostname  Action: update
 
  Returned a table with the following values:
 
  value  one two
 
  type    string
 
  msg    Hostname can contain alphanumerics only
 
 
# lua helloworld.lua view_resolver "Alpine"
 
  The following unhandled application error occured:
 
 
 
  controller: "hostname" does not have a "view_resolver" action.
 
  
 
= mvc load & exec special functions =
 
= mvc load & exec special functions =
Line 315: Line 359:
 
   This is the app controller's pre_exec function
 
   This is the app controller's pre_exec function
 
   This is the app controller's post_exec function
 
   This is the app controller's post_exec function
  This is the app controller's on_unload function
 
 
   Controller: hostname  Action: update
 
   Controller: hostname  Action: update
 
   Returned a table with the following values:
 
   Returned a table with the following values:
 
   value  Alpine
 
   value  Alpine
 
   type    string
 
   type    string
 +
  This is the app controller's on_unload function
  
 
We can add mvc functions to a specific controller, as well.  Add this to '''helloworld/app/hostname-controller.lua'''
 
We can add mvc functions to a specific controller, as well.  Add this to '''helloworld/app/hostname-controller.lua'''
Line 348: Line 392:
 
   This is the hostname controller's post_exec function
 
   This is the hostname controller's post_exec function
 
   This is the hostname controller's on_unload function
 
   This is the hostname controller's on_unload function
  This is the app controller's on_unload function
 
 
   Controller: hostname  Action: update
 
   Controller: hostname  Action: update
 
   Returned a table with the following values:
 
   Returned a table with the following values:
 
   value  Alpine
 
   value  Alpine
 
   type    string
 
   type    string
 +
  This is the app controller's on_unload function
  
 
Note that both the ''app'' and ''hostname'' '''on_load''' and '''on_unload''' functions were run, but only the ''hostname'' '''pre_exec''' and '''post_exec''' functions ran.  This is because the pre and post exec functions are run as part of the "action", and the '''dispatch''' function looks in the lowest-level controller for the pre/post_exec function.  Since ''hostname'' now defines those functions, it runs them.
 
Note that both the ''app'' and ''hostname'' '''on_load''' and '''on_unload''' functions were run, but only the ''hostname'' '''pre_exec''' and '''post_exec''' functions ran.  This is because the pre and post exec functions are run as part of the "action", and the '''dispatch''' function looks in the lowest-level controller for the pre/post_exec function.  Since ''hostname'' now defines those functions, it runs them.
Line 368: Line 412:
 
         print ("This is the hostname controller's pre_exec function")
 
         print ("This is the hostname controller's pre_exec function")
 
  end
 
  end
 +
 +
[[Category:ACF]] [[Category:Lua]]

Latest revision as of 21:50, 3 August 2016

Set the hostname with mvc.lua

In this example we will create a simple hostname-setting command-line application using mvc.lua. Once the controller/model are built, you can use the same code to set the hostname via the web with a web-based application controller.


For this example, we will assume you have root access on the linux box you are running on (preferably an Alpine Linux box!)

Get the mvc.lua module

Get the mvc.lua module from the git repository.

wget http://git.alpinelinux.org/cgit/acf-core/plain/lua/mvc.lua

Create a model and controller

Create a file hostname-controller.lua, defining the functions that an "end user" could run. We will only create one action, edithostname, which will be used to read and update the hostname. Since the action makes changes to the system, it naturally takes the form of a 'form':

hostname-controller.lua

-- Controller for editing hostname
local mymodule = {} 

mymodule.edithostname = function (self)
        return self.handle_form(self, self.model.get_hostname, self.model.set_hostname, self.clientdata, "Update", "Edit Hostname", "Hostname Updated")
end                                   

return mymodule

Create a file hostname-model.lua, defining the model functions to get and set the hostname. We return a cfe table for each function including the form with the one entry for hostname:

hostname-model.lua

-- Model functions for retrieving / setting the hostname
local mymodule = {}

-- Create a cfe defining the form for editing the hostname and containing the current value
mymodule.get_hostname = function(self, clientdata)
        local retval = cfe({ type="group", value={}, label="Hostname" })

        -- Warning - io.popen has security risks, never pass user data to io.popen
        local f = io.popen ("/bin/hostname")
        local n = f:read("*a") or "none"
        f:close()
        n=string.gsub(n, "\n$", "")

        retval.value.hostname = cfe({ value=n, label="Hostname" })

        return retval
end

-- Set the hostname from the value contained in the cfe created by get_hostname
mymodule.set_hostname = function(self, hostnameform, action)
        local success = true

        -- Check to make sure the name is valid
        if (hostnameform.value.hostname.value == "") then
                success = false
                hostnameform.value.hostname.errtxt = "Hostname must not be blank"
        elseif (#hostnameform.value.hostname.value > 16) then
                success = false
                hostnameform.value.hostname.errtxt = "Hostname must be 16 characters or less"
        elseif (string.find(hostnameform.value.hostname.value, "[^%w%_%-]")) then
                success = false
                hostnameform.value.hostname.errtxt = "Hostname can contain alphanumerics only"
        end

        -- If it is valid, set the hostname
        if ( success ) then
                local f = io.open("/etc/hostname", "w")
                if f then
                        f:write(hostnameform.value.hostname.value .. "\n")
                        f:close()
                end
                -- Warning - io.popen has security risks, never pass user data to io.popen
                f = io.popen ("/bin/hostname -F /etc/hostname")
                f:close()
        else
                hostnameform.errtxt = "Failed to set hostname"
        end

        return hostnameform
end

return mymodule

Optionally test the model code (without mvc.lua)

If you want, you can create a test.lua script to validate the model code works on its own:

test.lua

require("mvc") -- Needed for cfe function definition
m=require("hostname-model")

local form = m.get_hostname()
form.value.hostname.value = arg[1] or ""
form = m.set_hostname(nil, form)

if form.errtxt then
        print("FAILED: "..form.value.hostname.errtxt or form.errtxt)
end
form = m.get_hostname()
print(form.value.hostname.value)

You can then test this with:

#lua test.lua "Alpine"
 Alpine
#lua test.lua "Invalid Name"
 FAILED: Hostname can contain alphanumerics only
 Alpine

Add the package to the ACF framework

To make the model and controller work within the ACF mvc.lua framework, we must do several things.

1. Install the ACF core package:

apk add acf-core

Optionally, you can add the entire web-based ACF framework:

setup-acf

2. Modify the ACF configuration file to look in /etc/acf/app/ for additional packages. Edit the /etc/acf/acf.conf file to add the /etc/acf/app/ directory to the appdir comma-separated list:

appdir=/etc/acf/app/,/usr/share/acf/app/

3. Move the model and controller to the new package directory. We will call the package "test":

mkdir -p /etc/acf/app/test
mv hostname-*.lua /etc/acf/app/test

4. Test the new package using the acf-cli application:

# acf-cli /test/hostname/edithostname
result = {}
result["label"] = "Edit Hostname"
result["option"] = "Edit"
result["type"] = "form"
result["value"] = {}
result["value"]["hostname"] = {}
result["value"]["hostname"]["label"] = "Hostname"
result["value"]["hostname"]["type"] = "text"
result["value"]["hostname"]["value"] = "Alpine"
# acf-cli /test/hostname/edithostname hostname=test submit=true
result = {}
result["descr"] = "Hostname Updated"
result["label"] = "Edit Hostname"
result["option"] = "Edit"
result["type"] = "form"
result["value"] = {}
result["value"]["hostname"] = {}
result["value"]["hostname"]["label"] = "Hostname"
result["value"]["hostname"]["type"] = "text"
result["value"]["hostname"]["value"] = "test"

Note that the action string passed to acf-cli is of the form "/prefix/controller/action". The prefix is the path within /etc/acf/app where the controller may be found, so "/test/" for our case. The controller is determined from the controller file name, so "hostname" for our case. And, the action corresponds to the function within the controller to call, so "edithostname" for our case. Also note that, in the two examples above, the first case reads the existing hostname and the second case updates it. The output of the acf-cli application is a serialized version of the cfe form, which is good for testing but not too useful in real life.

Enable the package in the ACF web interface

1. Add the web-based ACF framework:

setup-acf

2. Configure all users to have access to the new hostname action. Edit "/etc/acf/app/test/hostname.roles" file and add GUEST permission for the edithostname action:

echo "GUEST=hostname/edithostname" > /etc/acf/app/test/hostname.roles

The new action should now be visible by browsing to https://IP-of-host/cgi-bin/acf/test/hostname/edithostname. Obviously you might want to reconsider providing GUEST access to this action, because this allows unauthenticated users to modify your hostname.

3. Add the new hostname action to the ACF menu. Edit "/etc/acf/app/test/hostname.menu" file and add a menu item:

echo "Test Hostname Edit edithostname" > /etc/acf/app/test/hostname.menu

You will need to log off from the ACF interface (or delete the session cookie) before the new menu item will be visible.

Make an MVC based application

You have two options for creating an MVC based application of your own.

1. Use the dispatch function. This is the method used by the web interface and the acf-cli application. The action to be dispatched is passed in a string format /prefix/controller/action and the input values are passed in as a clientinfo table. Output is determined by the view.

2. Use the new function to load the desired controller and then directly call the controller actions or model functions. Using the controller actions requires use of the clientdata structure to pass parameters. For calling the model functions, the application will call the get function to retrieve the form cfe, fill in the desired input values into the cfe, and then call the set function to submit the form and perform the desired action. The MVC application is responsible for any user interaction and display of the results.

Use the Dispatch method

test_dispatch

#!/usr/bin/lua
-- Simple CLI based mvc application

-- load the mvc module
mvc = require("acf.mvc")

-- create an new "mvc object"
MVC=mvc:new()

-- load the config file so we can find the appdir
MVC:read_config("acf")

-- dispatch the request
local clientdata = {hostname=arg[1], viewtype=arg[2], submit="Update"}
MVC:dispatch("/test/", "hostname", "edithostname", clientdata)

-- destroy the mvc object
MVC:destroy()

Test the application. As can be seen in the code above, the first argument is the new hostname and the second is a viewtype.

alpine:~# chmod 755 test_dispatch 
alpine:~# ./test_dispatch test
test:~# ./test_dispatch alpine
alpine:~#

Note that the application functions, but there is no output. This is because there is no viewtype supplied. There is built-in support for these standard viewtypes (not all apply): html, json, stream, serialized

alpine:~# ./test_dispatch test serialized
result = {}
result["descr"] = "Hostname Updated"
result["label"] = "Edit Hostname"
result["option"] = "Update"
result["type"] = "form"
result["value"] = {}
result["value"]["hostname"] = {}
result["value"]["hostname"]["label"] = "Hostname"
result["value"]["hostname"]["type"] = "text"
result["value"]["hostname"]["value"] = "test"
test:~# ./test_dispatch alpine json
{"type":"form","label":"Edit Hostname","value":{"hostname":{"value":"alpine","type":"text","label":"Hostname"}},"option":"Update","descr":"Hostname Updated"}
alpine:~# 

You can also use a custom viewtype and/or view to customize the output. This relies on haserl to parse the view file, and haserl lua functions are only available when launched from haserl scripts (it would be nice if haserl were available as a standalone lua library). Haserl scripts expect to be launched by a web browser to handle CGI data, so passing input is trickier. I'm sure there is a better way to do this, but this is just an example:

test_haserl

#!/usr/bin/haserl-lua5.2 --shell=lua
<%
-- Simple CLI based mvc application

-- load the mvc module
mvc = require("acf.mvc")

-- create an new "mvc object"
MVC=mvc:new()

-- load the config file so we can find the appdir
MVC:read_config("acf")

-- dispatch the request
local clientdata = {hostname=FORM.hostname, viewtype=FORM.viewtype, submit="Update"}
MVC:dispatch("/test/", "hostname", "edithostname", clientdata)

-- destroy the mvc object
MVC:destroy()
%>

/etc/acf/app/test/hostname-edithostname-text.lsp

<%
local data, viewlibrary, page_info, session = ... 

if data.errtxt then
        print(data:print_errtxt())
else
        print(data.descr)
end
%>

Test the application

test:~# chmod 755 test_haserl 
test:~# QUERY_STRING='hostname=alpine&viewtype=text' REQUEST_METHOD=GET ./test_haserl
Hostname Updated
alpine:~# QUERY_STRING='hostname=asdfasdfasdfasdfasdf&viewtype=text' REQUEST_METHOD=GET ./test_haserl
Failed to set hostname
hostname: Hostname must be 16 characters or less
alpine:~# 

You can further customize the application by creating your own application-specific controller. This is how the web interface and the acf-cli application are written. In both cases, a simple application is written to load not just an mvc:new() reference, but an application-specific controller to wrap the mvc:new() object. The dispatch function is then called on the application-specific controller object, which can override any function in mvc.lua to add a customized implementation. For example, both applications override the handle_clientdata function to customize the way input data is provided in the clientdata structure, and the web interface-specific controller contains a lot of code to handle features such as menus, templates, skins, authentication, redirection, ... The best way to understand this is to just read the code.

Use the New method

test_new

#!/usr/bin/lua
-- Simple CLI based mvc application

-- load the mvc module
mvc = require("acf.mvc")

-- create an new "mvc object"
MVC=mvc:new()

-- load the config file so we can find the appdir
MVC:read_config("acf")

-- load the hostname controller
HOSTNAME=MVC:new("/test/hostname")

local hostname

-- METHOD 1 - controller action
HOSTNAME.clientdata = {hostname=arg[1], submit="Update"}
hostname = HOSTNAME:edithostname()

-- METHOD 2 - model functions
hostname = HOSTNAME.model:get_hostname()
hostname.value.hostname.value = arg[1]
hostname = HOSTNAME.model:set_hostname(hostname)

if hostname.errtxt then
        print(hostname:print_errtxt())
else
        print(hostname.descr or "Hostname Updated")
end

HOSTNAME:destroy()
MVC:destroy()

Once you have a hostname controller object, you can access its actions, passing data through clientdata, or you can directly access the get/set functions of the model. Test the app:

test:~# chmod 755 test_new
test:~# ./test_new alpine
Hostname Updated
alpine:~# ./test_new asdfasdfasdfasdfasdf
Failed to set hostname
hostname: Hostname must be 16 characters or less
alpine:~# 


Out of date clock icon.svg
This material is obsolete ...

Please feel free to help us make an up-to-date version. (Discuss)

mvc load & exec special functions

The mvc.lua module has a provision for executing code on module load, prior to executing the controller's action, just after executing the controller's action, and on module unload.

This is done with the mvc table in the controller. To demonstrate, let's add a few functions to helloworld/app/app-controller.lua

mvc = {}
mvc.on_load = function (self, parent)
        print ("This is the app controller's on_load function")
end

mvc.pre_exec = function (self)
        print ("This is the app controller's pre_exec function")
end 

mvc.post_exec = function (self)
        print ("This is the app controller's post_exec function")
end
mvc.on_unload = function (self)
        print ("This is the app controller's on_unload function")
end

Now running our script shows when the functions get called:

# lua helloworld.lua update "Alpine"
  This is the app controller's on_load function
  This is the app controller's pre_exec function
  This is the app controller's post_exec function
  Controller: hostname  Action: update
  Returned a table with the following values:
  value   Alpine
  type    string
  This is the app controller's on_unload function

We can add mvc functions to a specific controller, as well. Add this to helloworld/app/hostname-controller.lua

mvc = {}
mvc.on_load = function (self, parent)
        print ("This is the hostname controller's on_load function")
end

mvc.pre_exec = function (self)
        print ("This is the hostname controller's pre_exec function")
end 

mvc.post_exec = function (self)
        print ("This is the hostname controller's post_exec function")
end

mvc.on_unload = function (self)
        print ("This is the hostname controller's on_unload function")
end

And this happens:

# lua helloworld.lua update "Alpine"
  This is the app controller's on_load function
  This is the hostname controller's on_load function
  This is the hostname controller's pre_exec function
  This is the hostname controller's post_exec function
  This is the hostname controller's on_unload function
  Controller: hostname  Action: update
  Returned a table with the following values:
  value   Alpine
  type    string
  This is the app controller's on_unload function

Note that both the app and hostname on_load and on_unload functions were run, but only the hostname pre_exec and post_exec functions ran. This is because the pre and post exec functions are run as part of the "action", and the dispatch function looks in the lowest-level controller for the pre/post_exec function. Since hostname now defines those functions, it runs them.

To run both the hostname and app pre_exec function, you must arrange for the hostname pre_exec function to call it's parent pre_exec:

mvc = {}
mvc.on_load = function (self, parent)
       print ("This is the hostname controller's on_load function")
       mvc.parent_pre_exec = parent.worker.mvc.pre_exec
end

mvc.pre_exec = function (self)
        mvc.parent_pre_exec (self)
        print ("This is the hostname controller's pre_exec function")
end