YaST/Web/Development/Tutorial
From openSUSE
| Revision as of 11:54, 6 May 2009 Bgeuken (Talk | contribs) Defining a new class � Previous diff |
Revision as of 13:44, 22 May 2009 Mvidner (Talk | contribs) typo fixes Next diff → |
||
| Line 14: | Line 14: | ||
| All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and | All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and | ||
| - | <pre> | + | |
| - | rest-service/webservice/plugins | + | cd rest-service/plugins |
| - | </pre> | + | |
| At least all modules should have the directories ''app config lib package tasks'' and ''test'', create them. Another way would be to copy the files of another directory like e.g. systemtime. | At least all modules should have the directories ''app config lib package tasks'' and ''test'', create them. Another way would be to copy the files of another directory like e.g. systemtime. | ||
| Line 26: | Line 26: | ||
| ==The model== | ==The model== | ||
| - | Now change to app/model and create the file security.rb. This is were you can define the security class with all it's attributes and methods. | + | Now change to app/model and create the file security.rb. This is where you can define the security class with all its attributes and methods. |
| Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running. | Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running. | ||
| Line 47: | Line 47: | ||
| xml.security do | xml.security do | ||
| xml.tag!(:ssh_running, @ssh_running) | xml.tag!(:ssh_running, @ssh_running) | ||
| - | xml.tag!(:firewall_runnning, @firewall_running) | + | xml.tag!(:firewall_running, @firewall_running) |
| xml.tag!(:running_on_startup, @running_on_startup) | xml.tag!(:running_on_startup, @running_on_startup) | ||
| xml.tag!(:error_id, @error_id, {:type => "integer"}) | xml.tag!(:error_id, @error_id, {:type => "integer"}) | ||
| Line 62: | Line 62: | ||
| ===Defining a new class=== | ===Defining a new class=== | ||
| Change directory to the controller and create the file security_controller.rb | Change directory to the controller and create the file security_controller.rb | ||
| - | cd ../controller | + | cd ../controller |
| - | vi security_controller.rb | + | vi security_controller.rb |
| ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController. | ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController. | ||
Revision as of 13:44, 22 May 2009
Contents |
Developing the module
Please notice that this site is momentarily under construction
This tutorial will show you how to create a module for the WebYast. The module will enable a user to start or stop the ssh-daemon and the firewall and additionally decide if the firewall shall be started after reboot.
While the rest-service take care for the functionality, the web-client gives the user a fine gui for the browser.
We will start with the rest-service.
Rest-Service:
After checking out the source from the git repo, try to run both, the rest-service and the web-client.
All modules of WebYast are stored in the plug-ins dirctory, so open a terminal and
cd rest-service/plugins
At least all modules should have the directories app config lib package tasks and test, create them. Another way would be to copy the files of another directory like e.g. systemtime.
It can be helpful to open the development log in a second terminal while developing on WebYast.
tail -fn 100 log/development.log
The model
Now change to app/model and create the file security.rb. This is where you can define the security class with all its attributes and methods. Like in every module security class should have error_id and error_string variables to assign different types of errors. We also need 3 variables containing boolean values which are running_on_startup, firewall_running and ssh_running.
Ruby's attr_accessor defines how to handle read/write access on our variables.
class Security
attr_accessor :error_id,
:error_string,
:running_on_startup,
:firewall_running,
:ssh_running
To respond on requests in xml or json format, we need to define some methods to do so.
def to_xml(options = {})
xml = options[:builder] ||= Builder::XmlMarkup.new(options)
xml.instruct! unless options[:skip_instruct]
xml.security do
xml.tag!(:ssh_running, @ssh_running)
xml.tag!(:firewall_running, @firewall_running)
xml.tag!(:running_on_startup, @running_on_startup)
xml.tag!(:error_id, @error_id, {:type => "integer"})
xml.tag!(:error_string, @error_string)
end
end
The method to_xml converts the attributes of the security class an sends them in xml format.
The controller
The controller connects the model on the service-side with representation on the web-client side. The web-client sends a GET-Request the rest-service and expects the current status as response. This can be either in xml or in json format like we defined in our model before.
Defining a new class
Change directory to the controller and create the file security_controller.rb
cd ../controller vi security_controller.rb
ApplicationController is part of the rest-service and provides some methods which we will need. So define the class SecurityController and derivate it from the ApplicationController. Require the scr library as well, it will enable you to access/use the yast api. The scr library is part of the rest-service and lies inside the lib directory.
include ApplicationHelper class SecurityController < ApplicationController require "scr"
The before_filter will execute login_required when any action where called. It checks if the user is logged in and redirects him, if not.
before_filter :login_required
Now let's define the show method which has to request GET requests and collects the data.
def show
@security = System::Security.new
if permission_check("org.opensuse.yast.webservice.read-security")
@security.firewall_running = get_firewall
@security.running_on_startup = get_firewall_after_startup
@security.ssh_running = get_ssh
Be aware that there isn't a Policy for the security module yet. Anyways permission_check verifies if a user has the necessary permission. How to create new policies is described here. The current status of firewall and ssh were collected by the private methods get_firewall, get_firewall_after_startup and get_ssh.
In case the check fails error_string will give a hint what was wrong.
else #no permissions
security.error_id = 1
security.error_string = "no permission"
end
Responding to the client
For every format in which we want to reply we need to define how. Now use the to_xml method defined in the model. It then returns the xml format which will be rendered for the response. Notice that also format.html render in xml format.
respond_to do |format|
format.html do
render :xml => @security.to_xml( :root => "security",
:dasherize => false ), :location => "none" #return xml value only
end
format.xml do
render :xml => @security.to_xml( :root => "security",
:dasherize => false ), :location => "none"
end
format.json do
render :json => @security.to_json , :location => "none"
end
end
end
end
Collecting the Status
We still need to define the get methods to collect the data. On the command-line someone would probably use rcsshd status and rcSuSEfirewall2 status to get the first two values. Why not using ruby's IO class to do exactly the same?
private
def firewall?
IO.popen("rcSuSEfirewall2 status", 'r+') do |pipe|
break if pipe.eof?
if pipe.read.to_str.include? "running"
return true
else#if retval.include? "unused"
return false
end
end
end
When the firewall is running the command sends back a string containing running, if not it contains unused. Check this to get the right state and store either true or false in a variable as return value. Mention that rubx on rails always expect the last value as return value. So no need to write anything like return ret.
get_ssh works nearly the same.
def ssh?
IO.popen("rcsshd status", 'r+') do |pipe|
break if pipe.eof?
if pipe.read.to_str.include? "running"
return true
else #if retval.include? "unused"
return false
end
end
Now the only thing missing is the firewall_after_startup? method. On the command-line we could use /sbin/yast2 firewall startup show to get the information, but this won't work with popen. It will only block the systembus and maybe create some zombies. So, let's use the scr-library of the rest-service. Again, the rest works nearly the same.
def firewall_after_startup?
cmd = Scr.execute(["/sbin/yast2", "firewall", "startup", "show"])
lines = cmd[:stderr].split "\n"
lines.each do |l|
if l.length > 0
if l.include? "enabled"
return true
elsif l.include? "manual"
return false
end
end
end
#ret
end
Defining routes
Adding some lines in config/routes.rb enables the rest-service to route the requests of the web-client to the right action:
ActionController::Routing::Routes.draw do |map|
map.resources :security, :controller => 'security'
map.connect "/security/:id", :controller => 'security', :action => 'singlevalue'
map.connect "/security/:id.xml", :controller => 'security', :action => 'singlevalue', :format =>'xml'
map.connect "/security/:id.html", :controller => 'security', :action => 'singlevalue', :format =>'html'
map.connect "/security/:id.json", :controller => 'security', :action => 'singlevalue', :format =>'json'
map.namespace :security do |security|
security.resource :dummy
end
map.resources :security do |security|
security.resources :commands
end
end
When a application receives a request, the routing will determine which controller and action to run.
Have a look in the browser
At this point, the rest-service can be watched inside a browser. Just make sure that you already defined and granted the security policies.
start the rest-service script/server webrick -p 3001
Now open a browser and go to http://localhost:3001/. You will see the different modules, but no the security module. That is because we didn't added it to the yast_controller yet. Open webservice/app/controller/yast_controller.rb and add the link for the security module.
link = Links.new
link.path = "security"
link.description = "Setting firewall and ssh-daemon"
if permission_check("org.opensuse.yast.webservice.read-security")
link.read_permission = true
else
link.read_permission = false
end
if permission_check("org.opensuse.yast.webservice.write-security")
link.write_permission = true
else
link.write_permission = false
end
link.execute_permission = false
link.delete_permission = false
link.new_permission = false
link.install_permission = false
@links << link
Now reload the browser page and try again. The before_filter in the controller redirects you to a login screen and you can put in your credentials. Normally this will be root. You can see the xml formated security instance.
Web-Client
Like on the service all modules are stored in the plugins dirctory. Create a security directory in the plugins directory and at least the subdirectories.
mkdir web-client/plugins/security cd web-client/plugins/security mkdir app package tasks test cd app mkdir app model view controller
The model
class Security < MyActiveResource
self.collection_name = "security"
self.element_name = "security"
end
The controller
Change to th controller dir
cd plugins/app/controller/ vi security_controller.rb
and define the SecurityController class which derivates from ApplicationController. Create a before_filter as well.
class SecurityController < ApplicationController before_filter :login_required
def index
set_permissions(controller_name)
@security = Security.find(:one, :from => '/security.xml')
set_permission checks the users permissions granted from the rest-service and sets them on the web-client side. Then we request the security.xml file and store their values in @security. The only thing left is to evaluate the values and prepare the variables for the view. This happens down below, where we store either "checked" or an empty string into them. In the view we later get a checked or an empty box to show for the web-frontend.
if @security.running_on_startup == true
@running_on_startup = "checked"
else
@running_on_startup = ""
end
if @security.firewall_running == true
@firewall_running = "checked"
else
@firewall_running = ""
end
if @security.ssh_running == true
@ssh_running = "checked"
else
@ssh_running = ""
end
end
The view
mkdir view/security vi view/security/index.rhtml
On the web-client side the view consists of 3 checkboxes, which are showing the current status of firewall, ssh and the automatic enabled firewall. To change the values one have to check or uncheck the boxes and transmit them with the Submit Settings button.
<h1><%=_("Security")%></h1>
<br>
<% form_tag '/security/commit' do %>
<div class="table">
<img src="/images/bg-th-left.gif" width="8" height="7" alt="" class="left" />
<img src="/images/bg-th-right.gif" width="7" height="7" alt="" class="right" />
<table class="listing form" cellpadding="0" cellspacing="0">
<tr>
<th class="full" colspan="2"><%=_("Settings")%></th>
</tr>
<tr class>
<td class="first"><strong><%=_("Enable Firewall Automatic Starting")%></strong></td>
<td class="last"><input type="checkbox" name="running_on_startup" value="true"
<%= @running_on_startup %> <%= "disabled=\"disabled\""
if @write_permission=="disabled" %> ></td>
</tr>
<tr class="bg">
<td class="first"><strong><%=_("Current Firewall Status")%> </strong></td><td class="last">
<input type="checkbox" name="firewall_running" value="true" <%= @firewall_running %>
<%= "disabled=\"disabled\"" if @write_permission=="disabled" %> ></td>
</tr>
<tr class>
<td class="first"><strong><%=_("Current SSH Status")%></strong></td>
<td class="last"><input type="checkbox" name="ssh_running" value="true" <%= @ssh_running %>
<%= "disabled=\"disabled\"" if @write_permission=="disabled" %> ></td>
</tr>
</table>
<p><%= submit_tag _("Submit Settings"), :onclick=>"Element.show('progress')", :disabled=>@write_permission -%></p>
</div>
<% end %>
The views/security/index.html.erb is nearly plain html. Only the variables for the checkboxes and permissions are embedded with ruby-code. By calling set_permissions in the controller, we have set @security-permissions variable. This line decides if the user is able to edit the checkbox or not.
<%= "disabled=\"disabled\"" if @write_permission=="disabled" %>
A look in the browser again
Now you can start the web-client and open a browser.
script/server
Go to http://localhost:3001, login and choose the security module. You can see the 3 options and can change them. After clicking the submit button, none of these changes will be sent. Actually we already added a commit action to the format tag,
<% form_tag '/security/commit' do %>
but haven't defined any commit action in the controller. Let's do this now.
Adding new action to the controller
Define a new method and create a new empty Security instance. The params hash contains the POST params of the view. Make a check for every variable and store true or false into the security instance.
def commit
s = Security.new()
if params[:running_on_startup] == "true"
s.running_on_startup = true
else
s.running_on_startup = false
end
if params[:firewall_running] == "true"
s.firewall_running = true
else
s.firewall_running = false
end
if params[:ssh_running] == "true"
s.ssh_running = true
else
s.ssh_running = false
end
The save method sends a POST with the security instance to /security. We still need to define what to respond from the rest-service side. The error_id and error_string can give valid informations about any error which might occur. So if anything went wrong, a noticeable error message made of the error_string will be shown. Else the user will get the notification that the settings have been written. In both cases the user got redirected to the view of index.html.erb
s.save
if s.error_id != 0
flash[:error] = s.error_string
redirect_to :action => :index
else
flash[:notice] = _('Settings have been written.')
redirect_to :action => :index
end
end
Rest-service Part 2
What's still missing is an action to respond to the POST request, when a user commits changes from the web-client. Therefor we define a create action inside the security_controller.
def create
respond_to do |format|
@security = System::Security.new()
if permission_check("org.opensuse.yast.webservice.write-security")
This time the user needs to be checked for his write permissions. If it succeeds the set_firewall, set_ssh and set_firewall_after_startup sets the new values for the class attributes.
if params[:security] != nil
@security.firewall = set_firewall params[:security][:firewall_running]
@security.ssh = set_ssh params[:security][:ssh_running]
@security.running_on_startup = set_running_on_startup params[:security][:running_on_startup]
logger.debug "UPDATED: #{@security.inspect}"
else
@security.error_id = 2
@security.error_string = "format or internal error"
end
else #no permission
@security.error_id = 1
@security.error_string = "no permission"
end
Finally create sends the new data of security class back to the client.
format.html do
render :xml => @security.to_xml( :root => "security",
:dasherize => false ), :location => "none" #return xml value only
end
format.xml do
render :xml => @security.to_xml( :root => "security",
:dasherize => false ), :location => "none"
end
format.json do
render :json => @security.to_json , :location => "none"
end
end
end
The methods set_firewall, set_firewall_after_startup and set_ssh are working nearly the same way. After evaluating the parameter, the firewall/sshd will set. The return value shows if the operation succeeded or not.
def firewall(param)
if param == true # start firewall
action = "restart"
else # stop firewall
action = "stop"
end
IO.popen("rcSuSEfirewall2 #{action}", 'r+') do |pipe|
break if pipe.eof?
retval = pipe.read.to_str
if retval.include? "done" #success
return true
end
end
return false
end
def ssh(param)
if param == true # start sshd
action = "restart"
else
action = "stop" # stop sshd
end
IO.popen("rcsshd #{action}", 'r+') do |pipe|
break if pipe.eof?
retval = pipe.read.to_str
if retval.include? "done" #success
return true
end
end
return false
end
def set_firewall_after_startup(param)
if param == true # enable
action = "atboot"
verify = "Enabling"
else # disable
action = "manual"
verify = "Removing"
end
cmd = Scr.execute(["/sbin/yast2", "firewall", "startup", "#{action}"])
lines = cmd[:stderr].split "\n"
lines.each do |l|
if l.length > 0
if l.include? "#{verify}"
return true
end
end
end
return false
end
In the end you have a full functional module to set up firewall and ssh-daemon via the web browser.
PolkitPolicies
Defining PolkitPolicies
All policies of WebYast are described in /usr/share/PolicyKit/policy/org.opensuse.yast.webservice.policy. To create a new policy like org.opensuse.yast.webservice.read-security you can add something to the file like
<action id="org.opensuse.yast.webservice.read-security">
<description>Reading firewall settings</description>
<message>Authentication is required to read firewall settings</message>
<defaults>
<allow_inactive>no</allow_inactive>
<allow_active>no</allow_active>
</defaults>
</action>
and to create org.opensuse.yast.webservice.write-security
<action id="org.opensuse.yast.webservice.write-security">
<description>Reading firewall settings</description>
<message>Authentication is required to read firewall settings</message>
<defaults>
<allow_inactive>no</allow_inactive>
<allow_active>no</allow_active>
</defaults>
</action>
Granting PolkitPolicies
Now we can grant the new policies to a user, eg. root
polkit-auth --user root –grant org.opensuse.yast.webservice.write-security polkit-auth --user root –grant org.opensuse.yast.webservice.read-security
After defining the policies it is possible to watch the actual status in the browser: http://localhost:3000/security. But there isn't any funtionality, new settings won't be transmitted. This will be enabled by adding the create method to the security_controller.rb of the rest-service.

