Home Wiki > openSUSE:WebYaST ExamplePlugin
Sign up | Login

openSUSE:WebYaST ExamplePlugin

tagline: From openSUSE

Developing an example plugin

This tutorial will show you how to create a module for WebYast. The module will a simple text editor which reads and writes the file located in /var/log/YaST2/example_file. Please consider that only the user root has access rights to /var/log/YaST2. So this example modules also shows how other users have access rights to this file too.

Example plugin for WebYaST >= 0.3

Setup the Environment

Currently there are different possibilities getting a running development:

The shorter way is the first one. So we are taking this one.

The short way...

... would be to install the example plugins via RPM and play with them. The needed package can be found here:

webyast-example

Please download and install the RPM which fits to your SuSE version and restart WebYaST with:

rcwebyast restart

The example plugin is ready for use now.

The long way...

...would be to create all plugin files manually, which are included in the RPM packages described above. This way takes more effort but shows more intense how WebYaST works.

Defining PolkitPolicies

Every Yast Module provides it's own policy file, which defines the actions a user can be granted. They are stored in /usr/share/polkit-1/actions/ and they usually have the form org.opensuse.yast.system.module_name.policy. For the example module we will need something like file write and read access, so create the org.opensuse.yast.system.example.policy file and add these lines to it:

	 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>

  <vendor>YaST Webservice Project</vendor>
  <vendor_url>http://en.opensuse.org/YAST</vendor_url>

  <action id="org.opensuse.yast.system.example.read">
    <description>Description of action</description>
    <message>Authentication message which appear on desktop (not affect webyast)</message>
    <defaults>
      <allow_inactive>no</allow_inactive>
      <allow_active>no</allow_active>
    </defaults>
  </action>

  <action id="org.opensuse.yast.system.example.write">
    <description>Description of action</description>
    <message>Authentication message which appear on desktop (not affect webyast)</message>
    <defaults>
      <allow_inactive>no</allow_inactive>
      <allow_active>no</allow_active>
    </defaults>
  </action>
</policyconfig>
	 

Granting PolkitPolicies

If you want to watch your module during development, you need to give some permissions to a user who is logged in into WebYaST, eg. root.

	 
/usr/sbin/grantwebyastrights --user root --action grant --policy org.opensuse.yast.system.example.read
/usr/sbin/grantwebyastrights --user root --action grant --policy org.opensuse.yast.system.example.write

As the WebYaST HTTP server will also access the file we will have to grant these permissions to the HTTP server user ( The WebYaST server is running under the user webyast).

	 
/usr/sbin/grantwebyastrights --user webyast --action grant --policy org.opensuse.yast.system.example.read
/usr/sbin/grantwebyastrights --user webyast --action grant --policy org.opensuse.yast.system.example.write

D-Bus Service

The D-Bus Service is running under root. This service handles the "real" file acess to /var/log/YaST2/example_file.

There are two ways of creating a D-Bus service. You can use existing YaST module and only create a D-Bus interface or you can write a standalone service.

As there is no YaST module for reading/writing files, a standalone, YaST-independent D-Bus service is used here.

D-Bus service: example.service

D-Bus interface: example.service.Interface

D-Bus object path: /org/example/service/Interface

Create the D-Bus service

The D-Bus service can be written in various languages. As WebYaST itself is written in Ruby, the example below is also in Ruby.

Put the following skeleton into your binary path of your choice, in order to match examples below, into /usr/local/sbin/exampleService.rb

The first part connects to the system bus and registers the service.

#!/usr/bin/env ruby
require 'rubygems'
require 'dbus'

# Choose the bus (could also be DBus::session_bus, which is not suitable for a system service)
bus = DBus::system_bus
# Define the service name
service = bus.request_service("example.service")

Create the object class and its interface. In the interface, create all methods.

In the example below, the interface provides two methods:

  • read, which reads specified file and returns it as a string (note that enclose in array is needed as DBus has multiple return values so it returns array of return values)
  • write, which writes specified string to the file.
class ExampleService < DBus::Object
  FILENAME = "/var/log/YaST2/example_file"
  # Create an interface.
  dbus_interface "example.service.Interface" do
    # Define D-Bus methods of the service
    # This method reads whole file which name it gets as a parameter and returns its contents
    dbus_method :read, "out contents:s" do
      out = ""
      begin
        File.open(FILENAME, "r") {|f| out = f.read }
      rescue
        out = "<empty>"
      end
      [out] #return value must be array, as DBus allow multiple return value, so it expect array of return values
    end
    # This method dumps a string into a file
    dbus_method :write, "in contents:s" do |contents|
      File.open(FILENAME, 'w') {|f| f.write(contents) }
      []
    end
  end
end

Create the object and export it through its path.

# Set the object path
obj = ExampleService.new("/org/example/service/Interface")
# Export it!
service.export(obj)

Run the event loop (in this example, the event loop never finishes).

# Now listen to incoming requests
main = DBus::Main.new
main << bus
main.run

Make this file executeable:

 chmod 755 /usr/local/sbin/exampleService.rb
Create needed permissions

Edit /etc/dbus-1/system.d/example.service.conf and add following contents in order to grant permission to access and register the service:

<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
  "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <policy user="root">
    <allow own="example.service" />
    <allow send_destination="example.service" />
  </policy>
<!-- yastws user is one which runs yast webservice and 
  he sends messages to service -->
  <policy user="webyast">
    <allow send_destination="example.service" />
    <!-- introspection is allowed -->
    <allow send_destination="example.service"
           send_interface="org.freedesktop.DBus.Introspectable" />
  </policy>
  <policy context="default">
    <deny send_destination="example.service"/>
    <deny send_destination="example.service"
           send_interface="org.freedesktop.DBus.Introspectable" />
  </policy>
</busconfig>

Without this file, you cannot even register the service in order to test it.

The configuration above allows root to register the service and the user webyast to use it. Therefore, the service cannot be used by any other users except webyast, whose permissions WebYaST uses.

NOTE: Please adapt this file if you want to run the WebYaST server under another user.

Starting the Service

To start the service automatically, create the service activation config file /usr/share/dbus-1/system-services/example.service.service with following contents:

# DBus service activation config
[D-BUS Service]
Name=example.service
Exec=/usr/local/sbin/exampleService.rb
User=root

Keep the [D-Bus Service] section name, only update the name of the service and the binary to execute.

Testing the service

To test the service, just call its methods. Following example shows how to call a D-Bus service. (Run this under user root)

#!/usr/bin/ruby
require 'rubygems'
require 'dbus'

bus = DBus.system_bus
ruby_service = bus.service("example.service")
obj = ruby_service.object("/org/example/service/Interface")
obj.introspect
obj.default_iface = "example.service.Interface"
puts obj.read

The service must be started - unless it is done automatically (see below), just run /usr/local/sbin/exampleService.rb as user root.

WebYaST rails plugin

Preparing

To start developing normally you will need the sources by check out the git repos and install the required packages. For more information have a look to the installation from source. In our example we are using the real WebYaST service which has been installed in /srv/www directory due the package installation.

All modules of WebYast are stored in the plug-ins directory, so open a terminal and create a new directory for your modul

  cd /srv/www/webyast/vendor/plugins
  mkdir example/
  cd example/

At least your new module should have the directories app config lib package tasks and test. Go on and create them.

mkdir app config lib package tasks test

Now change directory to app/ and create directories for the model, view and controller.

cd app
mkdir models views controllers

During development it can be helpful to open the production log in a second terminal.

tail -fn 100 /srv/www/webyast/log/passenger.log
The model

In the model you will define the example class with all it's attributes and methods. We will need only one variable for the content of the file.

Change directory to models/ and create the file example.rb.

The access to the variable can be defined with Ruby's attr_accessor. We will need read AND write access here.

class Example < BaseModel::Base
 attr_accessor :content

We don't inherit Example from ActiveRecord::Base, as we don't need a database. But in many cases with complex datastructure we need to define our own to_xml and to_json methods:

  def to_xml(options = {})
    xml = options[:builder] ||= Builder::XmlMarkup.new(options)
    xml.instruct! unless options[:skip_instruct]
..
..
  end

  def to_json(options = {})
    hash = Hash.from_xml(to_xml())
    return hash.to_json
  end

These methods convert the attributes of the given class into xml or json format and provide them as return value. Have a look to other plugins for more information.

In our example these methods are not needed cause the data structure is too simple. So we can use the standard to_xml and to_json methods of the BaseModel::Base class Now let's provide some functionality:

  def initialize
    load_content
  end

  def load_content
    @content = dbus_obj.read
  end

With Example.new() one can get an instance of Example class. At that point the content of the file has been already read and is available.

  def self.find(what=:one,options={})
    ret = Example.new
    ret.load_content
    ret
  end

Every model class should have a find methods in order to get the data. In our case it is the same like the initialize method but there are some other cases e.g. an user model where the find method is very useful:

  • User.find(:all) returns a array with all users
  • User.find('tux') returns an object of the user 'tux'.
  def update
    dbus_obj.write @content
  end

This method is used to write the content to the file. As you see we are using the DBUS-service for writing the file. Don't use IO methods at this point. It will bring you into trouble. As WebYast will run as a service it will have it's own user webyast. This user won't have root permissions, but get access to the system via the DBUS-service and the PolicyKit permissions we have defined before.

And finally, the dbus_obj helper, which reads and writes via the D-Bus service defined above:

  def dbus_obj
    bus = DBus.system_bus
    ruby_service = bus.service("example.service")
    obj = ruby_service.object("/org/example/service/Interface")
    obj.introspect
    obj.default_iface = "example.service.Interface"
    obj
  end
end # the end of the model class

Now, as we can read/write the file /var/log/YaST2/example_file we can go on with the controller.

The controller

The controller connects the model (described above) with the representation view (next chapter). The web-browser sends a GET-Request to WebYaST and expects the file content as response. This can be either in html,xml or json format like we defined in our model before. On the other hand the web-browser can send a POST-Request to WebYaST in order to change the file.

Change directory to the controllers and create the file example_controller.rb

 cd ../controllers
 vi example_controller.rb
class ExampleController < ApplicationController

  #require login before do any action
  before_filter :login_required

ApplicationController is part of WebYaST and provides some methods which we will need. So define the class ExampleController and derivate it from the ApplicationController. The before_filter will execute login_required when any action has been called. It checks if the user is logged in and redirects to the login screen if not.

  # Initialize GetText and Content-Type.
  init_gettext "webyast-example"

This part is only needed if you want to provide other languages/translations for your UI. For more information have a look to the Internationalization section.

Now let's define the show/index method which has to respond to GET requests and collects the data.

  def index
    permission_check "org.opensuse.yast.system.example.read"
    @example = Example.find

    respond_to do |format|
      format.xml  { render :xml => @example.to_xml}
      format.json { render :json => @example.to_json }
      format.html { @write_permission = permission_granted? "org.opensuse.yast.system.example.write"
                    render :index }
    end
  end

  def show
    index
  end

First permission_check checks the user for his rights. If he isn't allowed to read security informations, the show/index action rise an error 403 and stops. Else it creates a new Example object by calling its find method.

The show/index action responds the Example object by calling the view (described in the next chapter) if the request has the html format or returns the Example object in xml or json format.

And the second part would be to write values into the file via a POST request.

  def update
    permission_check "org.opensuse.yast.system.example.write"
    value = params["example"]
    if value.empty?
      raise InvalidParameters.new :example => "Missing"
    end
    @example = Example.find
    @example.content = value["content"] || ""
    error = nil
    begin
      @example.save
    rescue Exception => error
      Rails.logger.error "Error while saving file: #{error.inspect}"
    end
    respond_to do |format|
      format.xml  { render :xml => @example.to_xml}
      format.json { render :json => @example.to_json }
      format.html { unless error
                      flash[:notice] = _("File saved")
                    else
                      flash[:error] = _("Error while saving file: %s") % error.inspect
                    end
                    index }
    end
  end

  # See update
  def create
    update
  end

end #close the ExampleController class

After the permission check an parameter error will be raised if no parameter has been defined. In the case of a HTML request the method index will be called after writing the values in order to show it again. If it has been a XML or JSON request the current content of the saved file will be returned.

The view

Create a new directory called example inside the view and write a view for the index action.

cd ../views
mkdir example
vi example/index.erb

The view contains an text area only, which is showing the current content of the file. This value can be changed and saved by the Save button.

<div class="plugin-content plugin-border">
    <% form_tag '/example/update', :class => "webyast_form" do %>
      <div class="plugin-header">
        <div class="left">
          <label class="plugin-icon-container"><img class="plugin-icon" src="/icons/example_plugin.png"></label>
          <label class="plugin-name"><%=_("Example")%></label>
        </div>
        <div class="right" >
          <span id="questionMark" style="margin:2px 5px; float:none;">?</span>
        </div>
        <div class="clearfix"></div>
      </div>

      <div class="clearfix"> </div>
      <fieldset class="webyast_fieldset">
        <div class="fieldset_header">
          <span class="fieldset_header_title" >
            <%=_("Content of /var/log/YaST2/example_file")%>
          </span>
        </div>

        <div class="fieldset_body">
          <div class="row">
            <%= text_area(:example, :content, :cols => 125, :rows => 10) %>
          </div>
          <br>
        </div>
      </fieldset>

      <div class="button_container">
        <div class="hr"> </div>
        <div class="nav-buttons">
          <%= form_send_buttons :disabled => !@write_permission %>
        </div>
      </div>

    <% end %>
</div>

The views/example/index.erb is nearly plain html. Only few variables for the text_area, translations and permissions are embedded within ruby-code. By calling permission_granted? in the controller, we have set the @write_permission variable which is either true or false and decides if the user is able to edit the text area or not.

Defining Routes

Normally routing will be handled in the central file config/routes.rb. As this file belongs to the WebYaST base package we do not want to edit manually. So each plugin has his own rooting description defined in config/resources/example.yml inside the plugin/example directory.

cd ../../config
mkdir resources
vi resources/example.yml

The content of this file is:

#interface can be anything
interface: org.opensuse.yast.system.example
#controller is path on which can be service located
controller: example
#singular set if it is singular REST service or not
singular: true
Adding an startup icon for WebYaST

Go to plugins/example and create file called shortcuts.yml with following contents to add an icon for your module to the control center.

cd ..
vi shortcuts.yml

The file content would be:

main:
  icon: '/icons/example_plugin.png'
  url: /example
  groups: [ System ]
  title: _("Example")
  description: _("This is an example plugin only")
  read_permissions: [ org.opensuse.yast.system.example.read ]

Copy your own Icon to /srv/www/webyast/vendor/plugins/example/public/icons/example_plugin.png

You can reuse some WebYaST icons stored in /srv/www/webyast/public/icons .

The "_(....)" around each text string

This mechanism is used for tagging strings which are shown in the UI and which have to be provided in multiple languages. For more information have a look to the Internationalization section.

Testing

Now you can restart the WebYaST server by:

rcwebyast restart


Open a browser and go to http://localhost:4984, login with user root and choose the example module:


Webyast example.jpeg

Example plugin for WebYaST < 0.3

While the REST service takes care of the functionality, the web-client gives the user a fine GUI for the browser.

Setup the Environment

There are different possibilities getting a running development:

We are taking the second one (via appliance) cause it does not disturb your local environment. So you have to create an appliance and boot with the prefer utility (VMware, VirtualBox,...).

This tutorial describes two ways in order to show how an example plugin works.

The short way...

... would be to install the example plugins via RPM and play with them. The two packages are:

Please download and install the RPM which fits to your SuSE version and restart the services with:

rcyastws restart
rcyastwc restart

The example plugin is ready for use now.

The long way...

...would be to create all plugin files manually, which are included in the RPM packages described above. This way takes more effort but shows more intense how WebYaST works.

PolkitPolicit

Before we start with the program, we will define the permissions for the new module.

Defining PolkitPolicies

Every Yast Module provides it's own policy file, which defines the actions a user can be granted. They are stored in /usr/share/PolicyKit/policy/ and they usually have the form org.opensuse.yast.system.module_name.policy. For the example module we will need something like file write and read access, so create the org.opensuse.yast.system.example.policy file and add these lines to it:

	 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>

  <vendor>YaST Webservice Project</vendor>
  <vendor_url>http://en.opensuse.org/YAST</vendor_url>

  <action id="org.opensuse.yast.system.example.read">
    <description>Description of action</description>
    <message>Authentication message which appear on desktop (not affect webyast)</message>
    <defaults>
      <allow_inactive>no</allow_inactive>
      <allow_active>no</allow_active>
    </defaults>
  </action>

  <action id="org.opensuse.yast.system.example.write">
    <description>Description of action</description>
    <message>Authentication message which appear on desktop (not affect webyast)</message>
    <defaults>
      <allow_inactive>no</allow_inactive>
      <allow_active>no</allow_active>
    </defaults>
  </action>
</policyconfig>
	 
Granting PolkitPolicies

If you want to watch your module during development, you need to give some permissions to a user who is logged in into WebYaST, eg. root.

	 
polkit-auth --user root --grant org.opensuse.yast.system.example.write 
polkit-auth --user root --grant org.opensuse.yast.system.example.read

As the WebYaST REST service will also access the file we will have to grant these permissions to the REST service user ( The REST service is running under the user yastws).

	 
polkit-auth --user yastws --grant org.opensuse.yast.system.example.write 
polkit-auth --user yastws --grant org.opensuse.yast.system.example.read

D-Bus Service

The D-Bus Service is running under root. This service handles the "real" file acess to /var/log/YaST2/example_file.

There are two ways of creating a D-Bus service. You can use existing YaST module and only create a D-Bus interface or you can write a standalone service.

As there is no YaST module for reading/writing files, a standalone, YaST-independent D-Bus service is used here.

D-Bus service: example.service

D-Bus interface: example.service.Interface

D-Bus object path: /org/example/service/Interface


Create the D-Bus service

The D-Bus service can be written in various languages. As WebYaST itself is written in Ruby, the example below is also in Ruby.

Put the following skeleton into your binary path of your choice, in order to match examples below, into /usr/local/sbin/exampleService.rb

The first part connects to the system bus and registers the service.

#!/usr/bin/env ruby
require 'rubygems'
require 'dbus'

# Choose the bus (could also be DBus::session_bus, which is not suitable for a system service)
bus = DBus::system_bus
# Define the service name
service = bus.request_service("example.service")

Create the object class and its interface. In the interface, create all methods.

In the example below, the interface provides two methods:

  • read, which reads specified file and returns it as a string (note that enclose in array is needed as DBus has multiple return values so it returns array of return values)
  • write, which writes specified string to the file
class ExampleService < DBus::Object
  FILENAME = "/var/log/YaST2/example_file"
  # Create an interface.
  dbus_interface "example.service.Interface" do
    # Define D-Bus methods of the service
    # This method reads whole file which name it gets as a parameter and returns its contents
    dbus_method :read, "out contents:s" do
      out = ""
      begin
        File.open(FILENAME, "r") {|f| out = f.read }
      rescue
        out = "<empty>"
      end
      [out] #return value must be array, as DBus allow multiple return value, so it expect array of return values
    end
    # This method dumps a string into a file
    dbus_method :write, "in contents:s" do |contents|
      File.open(FILENAME, 'w') {|f| f.write(contents) }
      []
    end
  end
end

Create the object and export it through its path

# Set the object path
obj = ExampleService.new("/org/example/service/Interface")
# Export it!
service.export(obj)

Run the even loop (in this example, the event loop never finishes.

# Now listen to incoming requests
main = DBus::Main.new
main << bus
main.run

Make this file executeable:

 chmod 755 /usr/local/sbin/exampleService.rb
Create needed permissions

Edit /etc/dbus-1/system.d/example.service.conf and add following contents in order to grant permission to access and register the service:

<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
  "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <policy user="root">
    <allow own="example.service" />
    <allow send_destination="example.service" />
  </policy>
<!-- yastws user is one which runs yast webservice and 
  he sends messages to service -->
  <policy user="yastws">
    <allow send_destination="example.service" />
    <!-- introspection is allowed -->
    <allow send_destination="example.service"
           send_interface="org.freedesktop.DBus.Introspectable" />
  </policy>
  <policy context="default">
    <deny send_destination="example.service"/>
    <deny send_destination="example.service"
           send_interface="org.freedesktop.DBus.Introspectable" />
  </policy>
</busconfig>

Without this file, you cannot even register the service in order to test it.

The configuration above allows root to register the service and yastws to use it. Therefore, the service cannot be used by any other users except yastws, whose permissions the YaST web service uses.

Starting the Service

To start the service automatically, create the service activation config file /usr/share/dbus-1/system-services/example.service.service with following contents:

# DBus service activation config
[D-BUS Service]
Name=example.service
Exec=/usr/local/sbin/exampleService.rb
User=root

Keep the [D-Bus Service] section name, only update the name of the service and the binary to execute.

Testing the service

To test the service, just call its methods. Following example shows how to call a D-Bus service.

#!/usr/bin/ruby
require 'rubygems'
require 'dbus'

bus = DBus.system_bus
ruby_service = bus.service("example.service")
obj = ruby_service.object("/org/example/service/Interface")
obj.introspect
obj.default_iface = "example.service.Interface"
puts obj.read

The service must be started - unless it is done automatically (see below), just run /usr/local/sbin/exampleService.rb as root.

Rest-Service:

Preparing

To start developing normally you will need the sources by check out the git repos and install the required packages. For more information have a look to the installation from source. In our example we are using the real WebYaST services which have been installed in /srv/www directory due the package installation.

All modules of WebYast are stored in the plug-ins directory, so open a terminal and create a new directory for your modul

  cd /srv/www/yastws/vendor/plugins
  mkdir example/
  cd example/

At least your new module should have the directories app config lib package tasks and test. Go on and create them.

mkdir app config lib package tasks test

Now change directory to app/ and create directories for the model, view and controller.

cd app
mkdir models views controllers

During development it can be helpful to open the production log in a second terminal.

tail -fn 100 /srv/www/yastws/log/passenger.log
The model

In the model you will define the example class with all it's attributes and methods. We will need only one variable for the content of the file.

Change directory to models/ and create the file example.rb.

The access to the variable can be defined with Ruby's attr_accessor. We will need read AND write access here.

class Example < BaseModel::Base
 attr_accessor :content

We don't inherit Example from ActiveRecord::Base, as we don't need a database. But in many cases with complex datastructure we need to define our own to_xml and to_json methods:

  def to_xml(options = {})
    xml = options[:builder] ||= Builder::XmlMarkup.new(options)
    xml.instruct! unless options[:skip_instruct]
..
..
..
  end

  def to_json(options = {})
    hash = Hash.from_xml(to_xml())
    return hash.to_json
  end

These methods convert the attributes of the given class into xml or json format and provide them as return value. Have a look to other plugins for more information.

In our example these methods are not needed cause the data structure is too simple. So we can use the standard to_xml and to_json methods of the BaseModel::Base class

Now let's provide some functionality:

  def initialize
    load_content
  end

  def load_content
    @content = dbus_obj.read
  end

With Example.new() one can get an instance of Example class. At that point the content of the file has been already read and is available.

  def self.find(what=:one,options={})
    ret = Example.new
    ret.load_content
    ret
  end

Every model class should have a find methods in order to get the data. In our case it is the same like the initialize method but there are some other cases e.g. an user model where the find method is very useful:

  • User.find(:all) returns a array with all users
  • User.find('tux') returns an object of the user 'tux'.
  def update
    dbus_obj.write @content
  end

This method is used to write the content to the file. As you see we are using the DBUS-service for writing the file. Don't use IO methods at this point. It will bring you into trouble. As WebYast will run as a service it will have it's own user yastws. This user won't have root permissions, but get access to the system via the DBUS-service and the PolicyKit permissions we have defined before.

And finally, the dbus_obj helper, which reads and writes via the D-Bus service defined above:

  def dbus_obj
    bus = DBus.system_bus
    ruby_service = bus.service("example.service")
    obj = ruby_service.object("/org/example/service/Interface")
    obj.introspect
    obj.default_iface = "example.service.Interface"
    obj
  end

end # the end of the model class

Now, as we can read/write the file /var/log/YaST2/example_file we can go on with the controller.

The controller

The controller connects the model on the service-side with the representation on the web-client side. The web-client sends a GET-Request to the rest-service and expects the file content as response. This can be either in xml or in json format like we defined in our model before. On the other hand the web-client can send a POST-Request to the rest-service in order to change the file.

Change directory to the controllers and create the file example_controller.rb

 cd ../controllers
 vi example_controller.rb
class ExampleController < ApplicationController

  #require login before do any action
  before_filter :login_required

ApplicationController is part of the rest-service and provides some methods which we will need. So define the class ExampleController and derivate it from the ApplicationController. The before_filter will execute login_required when any action where called. It checks if the user is logged in and redirects to the login screen if not.

Now let's define the show method which has to respond to GET requests and collects the data.

  def show
    permission_check "org.opensuse.yast.system.example.read"
    example = Example.find

    respond_to do |format|
      format.xml  { render :xml => example.to_xml}
      format.json { render :json => example.to_json }
    end
  end

First permission_check checks the user for his rights. If he isn't allowed to read security informations, the show action rise an error 403 and stops. Else it creates a new Example object by calling its find method.

The show action responds the Example object to the web-client, which has to handle the display inside the browser. This will happen here. But first you have to define routes for your new module.

And the second part would be to write values into the file via a POST request.

  def update
    permission_check "org.opensuse.yast.system.example.write"
    value = params["example"]
    if value.empty?
      raise InvalidParameters.new :example => "Missing"
    end

    example = Example.find
    example.content = value["content"] || ""
    example.save
    show
  end

end #close the ExampleController class

After the permission check an parameter error will be raised if no parameter has been defined. After writing the value the method show will be called in order to return the current content of the saved file.

Defining Routes

The routes for every module is defined in config/resources/example.yml inside the plugin/example directory.

#interface can be anything
interface: org.opensuse.yast.system.example
#controller is path on which can be service located
controller: example
#singular set if it is singular REST service or not
singular: true
Testing

You can restart the REST service via calling rcyastws restart with root permissions.

Now you are able to watch your new module at http://localhost:4984 with your browser. The link name is the value of interface. You can also go directly to http://localhost:4984/example or type http://localhost:4984/example.xml to get a xml tree.

Another possibility would be to use curl from the command line:

curl -u root http://localhost:4984/example.xml

to get the values.

Web-Client

Like on the service all modules are stored in the plugins directory. Create a example directory in the plugins directory and at least the subdirectories view,controller and models in the app directory.

cd /srv/www/yast/vendor/plugins
mkdir example
cd example
mkdir app package tasks test config
cd app
mkdir models views controllers
The model

Create a simple model which only maps to the REST service:

cd models
vi example.rb

and add following contents

class Example < ActiveResource::Base
  extend YastModel::Base
  model_interface :"org.opensuse.yast.system.example"
end
The controller

Change to the controller directory and create the example_controller.rb file.

cd ../controllers/
vi example_controller.rb

Now define the ExampleController class and derivate it from ApplicationController and include yast/service_resource. Create a before_filter as well.

require 'yast/service_resource'
require 'example'

class ExampleController < ApplicationController
  before_filter :login_required, :read_permissions

This will start the login_required action like on the controller of the rest-service, but also starts the read_permissions action before executing any other action inside the security controller.

  private

  def read_permissions
    @permissions = Example.permissions
  end

This method reads the permissions and stores them in an instance variable so that they can be accessed by the other functions or by the view.

public

  # Initialize GetText and Content-Type.
  init_gettext "webyast-example-ui"

This part is only needed if you want to provide other languages/translations for your UI. For more information have a look to the Internationalization section.

  # GET /example
  def index
    @example = Example.find(:one)
  end

This defines the 'index' function which reads the file content via the model and the REST service and sets internal variables accordingly.

  # PUT /example
  def update
    @example = Example.find(:one)
    @example.content = params["example"]["content"]
    begin
      @example.save
      flash[:notice] = _("File saved")
    rescue Exception => error
      flash[:error] = _("Error while saving file: %s") % error.inspect
    end
    render :index
  end

end

By calling Example.find we request a new Example instance from the rest-service. The 'params' hash contains the POST parameter of the view. Then the update action (in the web-client controller, example-controller.rb) stores the 'params' which contains the new file content into the Example instance. The save method then sends a POST with the Example instance to the example module of the rest-service. If it responds with an error, the web-client flashes an error message.

At the end user got redirected to index page of the example plugin again.

The view

Create a new directory called security inside the view and write a view for the index action.

cd ../views
mkdir example
vi example/index.erb

On the web-client side the view contains an text area only, which is showing the current content of the file. This value can be changed and saved by the Save button.

<div class="plugin-content plugin-border">
    <% form_tag '/example/update', :class => "webyast_form" do %>
      <div class="plugin-header">
        <div class="left">
          <label class="plugin-icon-container"><img class="plugin-icon" src="/icons/example_plugin.png"></label>
          <label class="plugin-name"><%=_("Example")%></label>
        </div>
        <div class="right" >
          <span id="questionMark" style="margin:2px 5px; float:none;">?</span>
        </div>
        <div class="clearfix"></div>
      </div>

      <div class="clearfix"> </div>
      <fieldset class="webyast_fieldset">
        <div class="fieldset_header">
          <span class="fieldset_header_title" >
            <%=_("Content of /var/log/YaST2/example_file")%>
          </span>
        </div>

        <div class="fieldset_body">
          <div class="row">
            <%= text_area(:example, :content, :cols => 125, :rows => 10) %>
          </div>
          <br>
        </div>
      </fieldset>

      <div class="button_container">
        <div class="hr"> </div>
        <div class="nav-buttons">
          <%= form_send_buttons :disabled => !@permissions[:write] %>
        </div>
      </div>
    <% end %>
  </div>

The views/example/index.erb is nearly plain html. Only few variables for the text_area, translations and permissions are embedded within ruby-code. By calling Example.permissions in the controller, we have set @permissions variable. The @permissions[:write] contains either true or false and decides if the user is able to edit the text area or not.

Adding an startup icon for WebYaST

Go to plugins/example and create file called shortcuts.yml with following contents to add an icon for your module to the control center.


main:
  icon: '/icons/example_plugin.png'
  url: /example
  groups: [ System ]
  title: _("Example")
  description: _("This is an example plugin only")
  read_permissions: [ org.opensuse.yast.system.example.read ]

Copy your own Icon to /srv/www/yast/vendor/plugins/example/public/icons/example_plugin.png

The "_(....)" around each text string

This mechanism is used for tagging strings which are shown in the UI and which has to be provided in multiple languages. For more information have a look to the Internationalization section.

Testing

Now you can restart the web-client.

rcyastwc restart

Make sure that the rest-service is still running.

rcyastws status

Open a browser and go to http://localhost:54984, login with user root and choose the example module:


Webyast example.jpeg