Wednesday, August 7, 2013

Creating a MySQL db on Ubuntu as a normal user

Lately I tried to create a MySQL db on Ubuntu (version 11 which has MySQL 5.1 preinstalled). I was logged in under my normal username but I got a surprise when running the mysql_install_db command.
$ /usr/bin/mysql_install_db --datadir=./mysql/data
Installing MySQL system tables...

130806 22:17:21 [Warning] Can't create test file /home/andreash/mysql/data/andreas-Ub-2.lower-test
130806 22:17:21 [Warning] Can't create test file /home/andreash/mysql/data/andreas-Ub-2.lower-test

Installation of system tables failed!  Examine the logs in
./mysql/data for more information.
...

There were not log files though and checking directories and permissions didn't reveal any problems.
So I started to search and found that Ubuntu uses a security mechanism called apparmor which can be used to control certain aspects of an application.
In regards to MySQL that means that there exists a MySQL profile which defines which directories can be accessed (and how) by the MySQL programs. The profile for the daemon mysqld is defined in /etc/apparmor.d/usr.sbin.mysqld and looks like this:

# Last Modified: Tue Jun 19 17:37:30 2007
#include <tunables/global>

/usr/sbin/mysqld {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/user-tmp>
  #include <abstractions/mysql>
  #include <abstractions/winbind>

  capability dac_override,
  capability sys_resource,
  capability setgid,
  capability setuid,

  network tcp,

  /etc/hosts.allow r,
  /etc/hosts.deny r,

  /etc/mysql/*.pem r,
  /etc/mysql/conf.d/ r,
  /etc/mysql/conf.d/* r,
  /etc/mysql/*.cnf r,
  /usr/lib/mysql/plugin/ r,
  /usr/lib/mysql/plugin/*.so* mr,
  /usr/sbin/mysqld mr,
  /usr/share/mysql/** r,
  /var/log/mysql.log rw,
  /var/log/mysql.err rw,
  /var/lib/mysql/ r,
  /var/lib/mysql/** rwk,
  /var/log/mysql/ r,
  /var/log/mysql/* rw,
  /{,var/}run/mysqld/mysqld.pid w,
  /{,var/}run/mysqld/mysqld.sock w,

  /sys/devices/system/cpu/ r,

  # Site-specific additions and overrides. See local/README for details.
  #include <local/usr.sbin.mysqld>
}

So in order to enable MySQL to access a subdirectory of my $HOME I had to edit the file as root (sudo vi ...) and add this line to the list (I put it right under the /sys/devices line)

  /home/andreas/mysql/** rw,

The apparmor man page explains the syntax and attributes in detail. For my purposes it suffices to know that ** stands for the directory and all subdirectories underneath and rw is of course read/write.

Then this new profile needs to be activated replacing the old one via

$ sudo apparmor_parser -rv /etc/apparmor.d/usr.sbin.mysqld
Replacement succeeded for "/usr/sbin/mysqld".

Finally running the MySQL program again did create the databases.

Installing MySQL system tables...
OK
Filling help tables...
OK

To start mysqld at boot time you have to copy
support-files/mysql.server to the right place for your system

PLEASE REMEMBER TO SET A PASSWORD FOR THE MySQL root USER !
To do so, start the server, then issue the following commands:

...

Not knowing much about apparmor yet I wonder how one would go about to allow all users (on a bigger multi-user server) to use MySQL or any other application which is secured in the same way. It would be impractical to add all users home directories to the profile file so I guess there must be some shortcut. This needs more reading.

Friday, August 2, 2013

LibreOffice calc: automatically sort data upon data entry

A common task in LibreOffice (or Apache OpenOffice) calc is to sort a range of cells via Data -> Sort... where the sort criteria (columns, sort order) are defined.
If the spreadsheet is a living document where entries are deleted and new entries entered on a regular basis one would wish for this sorting to be done automatically rather than the user having to do it manually each time. In this article I show how to achieve this with macros in LibreOffice.

The very basic idea of automation is to assign a macro to an event. In case that event happens the macro is being run.

But before I get to that I'd like to outline the steps and how they are connected.

  • First define a range for the cells to be sorted
  • Then create the macro which will be triggered (it will make use of the range)
  • Assign the macro to the 'change content' event Now whenever the content of a cell is changed the macro will be triggered and run.

    Define a range

    Assume the data to be sorted are in two columns A and B. Currently I have only 4 entries but there could be more in the future. The columns also have a header in the first row.
    The range is set by
  • marking columns A and B
  • going to Data -> Define Range... and entering a name for the selection and finally clicking Add and OK. I chose the name 'MyData'.

    Create a macro

    It would suffice to create one macro but I opted to split the macro into two, one being of a more general reusable nature and the other more specific to the event.

    The first macro 'SortRange' sorts a range by its second column in descending order using the first row as header i.e. not part of the actual sort. The macro gets a range object as parameter and thus could be used for any range.

    Sub SortRange( oRange As Variant )     
    rem Sorts range 'sRange' by column 2 in descending order
    rem (assumes that columns have labels in first row)
    
    rem ----------------------------------------------------------------------
    rem define variables
    dim document   as object
    dim dispatcher as object
    rem ----------------------------------------------------------------------
    rem get access to the document
    document   = ThisComponent.CurrentController.Frame
    dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
    
    rem ----------------------------------------------------------------------
    rem Select the range
    dim args1(0) as new com.sun.star.beans.PropertyValue
    args1(0).Name = "ToPoint"
    args1(0).Value = oRange.AbsoluteName
    
    dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args1())
    
    rem ----------------------------------------------------------------------
    dim args2(7) as new com.sun.star.beans.PropertyValue
    args2(0).Name = "ByRows"
    args2(0).Value = true
    args2(1).Name = "HasHeader"
    args2(1).Value = true
    args2(2).Name = "CaseSensitive"
    args2(2).Value = false
    args2(3).Name = "NaturalSort"
    args2(3).Value = false
    args2(4).Name = "IncludeAttribs"
    args2(4).Value = true
    args2(5).Name = "UserDefIndex"
    args2(5).Value = 0
    
    Rem Sort by the second column of the range !!!
    args2(6).Name = "Col1"
    args2(6).Value = oRange.RangeAddress.StartColumn + 2
    
    args2(7).Name = "Ascending1"
    args2(7).Value = false
    
    dispatcher.executeDispatch(document, ".uno:DataSort", "", 0, args2())
    
    End Sub
    
    Note how the Data -> Sort... Options are mapped to these properties and could be adjusted if one wanted different sorting criteria.

    The second macro 'SortRangeFilter' checks whether the change actually happened inside the range and then calls the SortRange macro. The name of the range is hardcoded in this macro and thus makes it inflexible but I couldn't find a better way (yet).
    Macros which are triggered by an event listener get passed a parameter which describes the source object. These can be different things which I need to differentiate in my code:

  • a single cell e.g. when entering a value
  • a range of cells e.g. when deleting a row
  • a set of ranges e.g. when marking and deleting two disjunct cells
    In the last cases my selection might contain cells which are not members of my defined range. In order to make my code the most flexible I apply the following logic:
  • a single cell is just a special case of range of cells thus I only need to differentiate two cases
  • for the remaining cases I determine the intersection of my named range with the selection (= source of the event). If it is empty there is nothing to do, otherwise I need to sort the range.

    Sub SortRangeFilter( oEvent As Variant )
    
    Dim sRange As String
    sRange = "MyData"
    Rem       ^^^^^^
    Rem       Hardcoded range name
    
    Rem Get the range object in order to access its boundaries
    Dim oSheet As Variant
    oSheet = ThisComponent.CurrentController.getActiveSheet()
    
    Rem Define an error handler in case the range name does not exist
    On Error Goto ErrorHandler
    Dim oCellRange As Variant
    oCellRange = oSheet.getCellRangeByName( sRange )
    
    Rem Check the type of 'oEvent'
    Rem Both support the 'queryIntersection' method
    If ( oEvent.supportsService( "com.sun.star.sheet.SheetCellRanges" ) Or _
         oEvent.supportsService( "com.sun.star.sheet.SheetCellRange" )) Then
      If oEvent.queryIntersection( oCellRange.RangeAddress ).count > 0 Then
        SortRange( oCellRange )
      Endif
    Endif
    Exit Sub
    
    ErrorHandler:
      Msgbox "SortRangeFilter: Range does not exist '" + sRange + "'"
      Exit Sub
    End Sub
    
    Note: rather than hardcoding the range name one could check all defined ranges for the current sheet and try to find the right one but there are a number of drawbacks:
  • a cell could be a member of several ranges (since ranges can overlap). Which range should be picked for sorting?
  • if multiple cells are selected (e.g. a row) there is an even higher chance of confusion. Some of the selected cells might be in range 1, others in range 2 etc. so which range is to be picked? And it is unlikely that all ranges need to be sorted.

    Both macros need to be entered in a macro library. I chose My Macros -> Standard -> Module1.

    Set event listener

    Now that the macro is in place I can set up the event trigger.
  • Right click on the sheet name and choose Sheet Events...
  • In the Assign Action window select Content changed and click Macro...
  • In the Macro Selector window navigate to (in my case) My Macros -> Standard -> Module1 and select SortRange in the list of macros and click OK
  • Back again in the Assign Action window click OK too

    Now you have it: any change in the range i.e. columns A or B will trigger a re-sort. Any other change on the sheet won't.
    If you create the range in a different place the sort will still be triggered.



    Below I'll show a couple of scenarios and how the macros behave. Assume there is a defined range 'MyData' somewhere on the spreadsheet, say C5:D10. The column headers have been entered in B5 and C5.

    Then the sheet event trigger needs to be assigned as described above and I can start entering the data.

    I begin with B6=A and C6=20. Nothing happens other than the named range being marked due to the macro.

    Then I enter B7=B and C7=34 and now the sort trigger already fires and sorts my columns and reverses the order.

    I enter another data set B8=C and C8=51
    And finally the last data B9=D and C9=40 which is moved up two rows.

    Always starting with the last setup I show various scenarios to delete cells and how the sort automatically kicks in.

    Mark a single cell and delete it
    Mark a row and delete it
    Mark some disjunct cells and delete them
    If you accidentally delete the range 'MyData' you get an error message

    Since my macro selects the data range it stays this way after the sort has finished. Of course this could be changed but I view it as an indicator that something happened, the viewer is visually attracted to the marked data range.