I have made the following changes intended for :
  CE:MW:Shared / ssu

Please review and accept or decline.
BOSS has already run some checks on this request.
See the "Messages from BOSS" section below.

https://build.pub.meego.com//request/show/8446

Thank You,
aard

[This message was auto-generated]

---

Request # 8446:

Messages from BOSS:

State: review at 2013-03-22T13:02:40 by bossbot

Reviews:
       accepted by bossbot : Prechecks succeeded.
       new for CE-maintainers : Please replace this text with a review and 
approve/reject the review (not the SR). BOSS will take care of the rest

Changes:
  submit: home:aard:branches:CE:MW:Shared / ssu -> CE:MW:Shared / ssu
  
changes files:
--------------
--- ssu.changes
+++ ssu.changes
@@ -0,0 +1,20 @@
+* Fri Mar 22 2013 Bernd Wachter <[email protected]> - 0.25
+- Add variable substitution
+
+* Thu Mar 21 2013 Bernd Wachter <[email protected]> - 0.24
+- Add support for arbitrary variables in flavour groups
+- Split off device info and settings functionality
+- Add support for board-mappings.d
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.23
+- Add force option to rndssu update
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.22
+- Add systemd logging to core library
+- Move zypper plugin logging to core library
+- Fix bug where resolverplugin does not return on credentials update
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.21
+- Reduce time before credentials get refreshed
+- Add logging for url resolver
+

old:
----
  ssu-0.20.tar.gz

new:
----
  ssu-0.25.tar.gz

spec files:
-----------
--- ssu.spec
+++ ssu.spec
@@ -1,9 +1,9 @@
 Name: ssu
-Version: 0.20
+Version: 0.25
 Release: 1
 Summary: SSU enabler for RND
 Group: System/Base
-License: Proprietary
+License: GPLv2
 Source0: %{name}-%{version}.tar.gz
 URL: https://github.com/nemomobile/ssu
 BuildRequires: pkgconfig(QtCore)
@@ -93,7 +93,7 @@
 
 
 %prep
-%setup -q
+%setup -q -n %{name}-%{version}
 
 
 %build

other changes:
--------------

++++++ ssu-0.20.tar.gz -> ssu-0.25.tar.gz
--- constants.h
+++ constants.h
@@ -18,6 +18,8 @@
 #define SSU_DEFAULT_CONFIGURATION "/usr/share/ssu/ssu-defaults.ini"
 /// Path to board / device family mappings file
 #define SSU_BOARD_MAPPING_CONFIGURATION "/usr/share/ssu/board-mappings.ini"
+/// Path to config.d for board mappings
+#define SSU_BOARD_MAPPING_CONFIGURATION_DIR "/usr/share/ssu/board-mappings.d"
 /// The SSU protocol version used by the ssu client libraries
 #define SSU_PROTOCOL_VERSION "1"
 #endif
--- libssu/libssu.pro
+++ libssu/libssu.pro
@@ -1,13 +1,22 @@
 BUILD = ../build/libssu
 HEADERS = ssu.h \
+        ssudeviceinfo.h \
+        ssulog.h \
+        ssuvariables.h \
+        ssusettings.h \
         ../constants.h
-SOURCES = ssu.cpp
+SOURCES = ssu.cpp \
+        ssudeviceinfo.cpp \
+        ssulog.cpp \
+        ssuvariables.cpp \
+        ssusettings.cpp
 TEMPLATE = lib
 TARGET = ssu
-CONFIG += dll mobility
+CONFIG += dll mobility link_pkgconfig
 QT -= gui
 QT += network xml
 MOBILITY += systeminfo
+PKGCONFIG += libsystemd-journal
 
 headers.files = ssu.h
 headers.path  = /usr/include
--- libssu/ssu.cpp
+++ libssu/ssu.cpp
@@ -5,16 +5,18 @@
  * @date 2012
  */
 
-#include <QSystemDeviceInfo>
-
 #include <QtXml/QDomDocument>
+
 #include "ssu.h"
-#include "../constants.h"
+#include "ssulog.h"
+#include "ssuvariables.h"
 
-QTM_USE_NAMESPACE
+#include "../constants.h"
 
-Ssu::Ssu(): QObject(){
+Ssu::Ssu(QString fallbackLog): QObject(){
   errorFlag = false;
+  fallbackLogPath = fallbackLog;
+  pendingRequests = 0;
 
 #ifdef SSUCONFHACK
   // dirty hack to make sure we can write to the configuration
@@ -29,83 +31,8 @@
   }
 #endif
 
-  settings = new QSettings(SSU_CONFIGURATION, QSettings::IniFormat);
+  settings = new SsuSettings(SSU_CONFIGURATION, QSettings::IniFormat, 
SSU_DEFAULT_CONFIGURATION);
   repoSettings = new QSettings(SSU_REPO_CONFIGURATION, QSettings::IniFormat);
-  boardMappings = new QSettings(SSU_BOARD_MAPPING_CONFIGURATION, 
QSettings::IniFormat);
-  QSettings defaultSettings(SSU_DEFAULT_CONFIGURATION, QSettings::IniFormat);
-
-  int configVersion=0;
-  int defaultConfigVersion=0;
-  if (settings->contains("configVersion"))
-    configVersion = settings->value("configVersion").toInt();
-  if (defaultSettings.contains("configVersion"))
-    defaultConfigVersion = defaultSettings.value("configVersion").toInt();
-
-  if (configVersion < defaultConfigVersion){
-    qDebug() << "Configuration is outdated, updating from " << configVersion
-             << " to " << defaultConfigVersion;
-
-    for (int i=configVersion+1;i<=defaultConfigVersion;i++){
-      QStringList defaultKeys;
-      QString currentSection = QString("%1/").arg(i);
-
-      qDebug() << "Processing configuration version " << i;
-      defaultSettings.beginGroup(currentSection);
-      defaultKeys = defaultSettings.allKeys();
-      defaultSettings.endGroup();
-      foreach (const QString &key, defaultKeys){
-        // Default keys support both commands and new keys
-        if (key.compare("cmd-remove", Qt::CaseSensitive) == 0){
-          // Remove keys listed in value as string list
-          QStringList oldKeys = defaultSettings.value(currentSection + 
key).toStringList();
-          foreach (const QString &oldKey, oldKeys){
-            if (settings->contains(oldKey)){
-              settings->remove(oldKey);
-              qDebug() << "Removing old key:" << oldKey;
-            }
-          }
-        } else if (!settings->contains(key)){
-          // Add new keys..
-          settings->setValue(key, defaultSettings.value(currentSection + key));
-          qDebug() << "Adding new key: " << key;
-        } else {
-          // ... or update the ones where default values has changed.
-          QVariant oldValue;
-
-          // check if an old value exists in an older configuration version
-          for (int j=i-1;j>0;j--){
-            if (defaultSettings.contains(QString("%1/").arg(j)+key)){
-              oldValue = defaultSettings.value(QString("%1/").arg(j)+key);
-              break;
-            }
-          }
-
-          // skip updating if there is no old value, since we can't check if 
the
-          // default value has changed
-          if (oldValue.isNull())
-            continue;
-
-          QVariant newValue = defaultSettings.value(currentSection + key);
-          if (oldValue == newValue){
-            // old and new value match, no need to do anything, apart from 
beating the
-            // person who added a useless key
-            continue;
-          } else {
-            // default value has changed, so check if the configuration is 
still
-            // using the old default value...
-            QVariant currentValue = settings->value(key);
-            // testcase: handles properly default update of thing with changed 
value in ssu.ini?
-            if (currentValue == oldValue){
-              // ...and update the key if it does
-              settings->setValue(key, newValue);
-              qDebug() << "Updating " << key << " from " << currentValue << " 
to " << newValue;
-            }
-          }
-        }
-      }
-      settings->setValue("configVersion", i);
-    }
-  }
 
 #ifdef TARGET_ARCH
   if (!settings->contains("arch"))
@@ -144,117 +71,6 @@
     return 
"your-configuration-is-broken-and-does-not-contain-credentials-url-for-" + 
scope;
 }
 
-QString Ssu::deviceFamily(){
-  QString model = deviceModel();
-
-  if (!cachedFamily.isEmpty())
-    return cachedFamily;
-
-  cachedFamily = "UNKNOWN";
-
-  if (boardMappings->contains("variants/" + model))
-    model = boardMappings->value("variants/" + model).toString();
-
-  if (boardMappings->contains(model + "/family"))
-    cachedFamily = boardMappings->value(model + "/family").toString();
-
-  return cachedFamily;
-}
-
-QString Ssu::deviceModel(){
-  QDir dir;
-  QFile procCpuinfo;
-  QStringList keys;
-
-  if (!cachedModel.isEmpty())
-    return cachedModel;
-
-  boardMappings->beginGroup("file.exists");
-  keys = boardMappings->allKeys();
-
-  // check if the device can be identified by testing for a file
-  foreach (const QString &key, keys){
-    QString value = boardMappings->value(key).toString();
-    if (dir.exists(value)){
-      cachedModel = key;
-      break;
-    }
-  }
-  boardMappings->endGroup();
-  if (!cachedModel.isEmpty()) return cachedModel;
-
-  // check if the QSystemInfo model is useful
-  QSystemDeviceInfo devInfo;
-  QString model = devInfo.model();
-  boardMappings->beginGroup("systeminfo.equals");
-  keys = boardMappings->allKeys();
-  foreach (const QString &key, keys){
-    QString value = boardMappings->value(key).toString();
-    if (model == value){
-      cachedModel = key;
-      break;
-    }
-  }
-  boardMappings->endGroup();
-  if (!cachedModel.isEmpty()) return cachedModel;
-
-  // check if the device can be identified by a string in /proc/cpuinfo
-  procCpuinfo.setFileName("/proc/cpuinfo");
-  procCpuinfo.open(QIODevice::ReadOnly | QIODevice::Text);
-  if (procCpuinfo.isOpen()){
-    QTextStream in(&procCpuinfo);
-    QString cpuinfo = in.readAll();
-    boardMappings->beginGroup("cpuinfo.contains");
-    keys = boardMappings->allKeys();
-
-    foreach (const QString &key, keys){
-      QString value = boardMappings->value(key).toString();
-      if (cpuinfo.contains(value)){
-        cachedModel = key;
-        break;
-      }
-    }
-    boardMappings->endGroup();
-  }
-  if (!cachedModel.isEmpty()) return cachedModel;
-
-
-  // check if there's a match on arch ofr generic fallback. This probably
-  // only makes sense for x86
-  boardMappings->beginGroup("arch.equals");
-  keys = boardMappings->allKeys();
-  foreach (const QString &key, keys){
-    QString value = boardMappings->value(key).toString();
-    if (settings->value("arch").toString() == value){
-      cachedModel = key;
-      break;
-    }
-  }
-  boardMappings->endGroup();
-  if (cachedModel.isEmpty()) cachedModel = "UNKNOWN";
-
-  return cachedModel;
-}
-
-QString Ssu::deviceUid(){
-  QString IMEI;
-  QSystemDeviceInfo devInfo;
-
-  IMEI = devInfo.imei();
-  // this might not be completely unique (or might change on reflash), but 
works for now
-  if (IMEI == ""){
-    if (deviceFamily() == "n950-n9" || deviceFamily() == "n900"){
-      bool ok;
-      QString IMEIenv = getenv("imei");
-      IMEIenv.toLongLong(&ok, 10);
-      if (ok && (IMEIenv.length() == 16 || IMEIenv.length() == 15))
-        IMEI = IMEIenv;
-    } else
-      IMEI = devInfo.uniqueDeviceID();
-  }
-  return IMEI;
-}
-
 bool Ssu::error(){
   return errorFlag;
 }
@@ -292,6 +108,7 @@
 bool Ssu::registerDevice(QDomDocument *response){
   QString certificateString = 
response->elementsByTagName("certificate").at(0).toElement().text();
   QSslCertificate certificate(certificateString.toAscii());
+  SsuLog *ssuLog = SsuLog::instance();
 
   if (certificate.isNull()){
     // make sure device is in unregistered state on failed registration
@@ -314,7 +131,7 @@
   // oldUser is just for reference purposes, in case we want to notify
   // about owner changes for the device
   QString oldUser = 
response->elementsByTagName("user").at(0).toElement().text();
-  qDebug() << "Old user:" << oldUser;
+  ssuLog->print(LOG_DEBUG, QString("Old user for your device was: 
%1").arg(oldUser));
 
   // if we came that far everything required for device registration is done
   settings->setValue("registered", true);
@@ -335,30 +152,29 @@
 QString Ssu::repoUrl(QString repoName, bool rndRepo, QHash<QString, QString> 
repoParameters){
   QString r;
   QStringList configSections;
-  QStringList repoVariables;
+  SsuVariables var;
 
   errorFlag = false;
 
   // fill in all arbitrary variables from ssu.ini
-  settings->beginGroup("repository-url-variables");
-  repoVariables = settings->allKeys();
-  foreach (const QString &key, repoVariables){
-    repoParameters.insert(key, settings->value(key).toString());
-  }
-  settings->endGroup();
+  var.resolveSection(settings, "repository-url-variables", &repoParameters);
 
   // add/overwrite some of the variables with sane ones
   if (rndRepo){
     repoParameters.insert("flavour", 
repoSettings->value(flavour()+"-flavour/flavour-pattern").toString());
     repoParameters.insert("flavourPattern", 
repoSettings->value(flavour()+"-flavour/flavour-pattern").toString());
     repoParameters.insert("flavourName", flavour());
-    repoParameters.insert("release", settings->value("rndRelease").toString());
     configSections << flavour()+"-flavour" << "rnd" << "all";
+
+    // Make it possible to give any values with the flavour as well.
+    // These values can be overridden later with domain if needed.
+    var.resolveSection(repoSettings, flavour()+"-flavour", &repoParameters);
   } else {
-    repoParameters.insert("release", settings->value("release").toString());
     configSections << "release" << "all";
   }
 
+  repoParameters.insert("release", release(rndRepo));
+
   if (!repoParameters.contains("debugSplit"))
     repoParameters.insert("debugSplit", "packages");
 
@@ -366,28 +182,24 @@
     repoParameters.insert("arch", settings->value("arch").toString());
 
   repoParameters.insert("adaptation", 
settings->value("adaptation").toString());
-  repoParameters.insert("deviceFamily", deviceFamily());
-  repoParameters.insert("deviceModel", deviceModel());
+  repoParameters.insert("deviceFamily", deviceInfo.deviceFamily());
+  repoParameters.insert("deviceModel", deviceInfo.deviceModel());
+
+  QStringList keys;
+  keys << "chip" << "adaptation" << "vendor";
+
+  foreach(QString key, keys){
+    QString value;
+    if (deviceInfo.getValue(key,value))
+      repoParameters.insert(key, value);
+  }
 
   // Domain variables
   // first read all variables from default-domain
-  repoSettings->beginGroup("default-domain");
-  QStringList defKeys = repoSettings->allKeys();
-  foreach (const QString &key, defKeys){
-      repoParameters.insert(key, repoSettings->value(key).toString());
-  }
-  repoSettings->endGroup();
+  var.resolveSection(repoSettings, "default-domain", &repoParameters);
+
   // then overwrite with domain specific things if that block is available
-  QString domainSection = domain() + "-domain";
-  QStringList sections = repoSettings->childGroups();
-  if (sections.contains(domainSection)){
-    repoSettings->beginGroup(domainSection);
-    QStringList domainKeys = repoSettings->allKeys();
-    foreach (const QString &key, domainKeys){
-      repoParameters.insert(key, repoSettings->value(key).toString());
-    }
-    repoSettings->endGroup();
-  }
+  var.resolveSection(repoSettings, domain()+"-domain", &repoParameters);
 
   if (settings->contains("repository-urls/" + repoName))
     r = settings->value("repository-urls/" + repoName).toString();
@@ -403,25 +215,19 @@
     }
   }
 
-  QHashIterator<QString, QString> i(repoParameters);
-  while (i.hasNext()){
-    i.next();
-    r.replace(
-      QString("%(%1)").arg(i.key()),
-      i.value());
-  }
-
-  return r;
+  return var.resolveString(r, &repoParameters);
 }
 
 void Ssu::requestFinished(QNetworkReply *reply){
   QSslConfiguration sslConfiguration = reply->sslConfiguration();
+  SsuLog *ssuLog = SsuLog::instance();
 
-  qDebug() << 
sslConfiguration.peerCertificate().issuerInfo(QSslCertificate::CommonName);
-  qDebug() << 
sslConfiguration.peerCertificate().subjectInfo(QSslCertificate::CommonName);
+  ssuLog->print(LOG_DEBUG, QString("Certificate used was issued for '%1' by 
'%2'. Complete chain:")
+               
.arg(sslConfiguration.peerCertificate().subjectInfo(QSslCertificate::CommonName))
+               
.arg(sslConfiguration.peerCertificate().issuerInfo(QSslCertificate::CommonName)));
 
   foreach (const QSslCertificate cert, 
sslConfiguration.peerCertificateChain()){
-    qDebug() << "Cert from chain" << 
cert.subjectInfo(QSslCertificate::CommonName);
+    ssuLog->print(LOG_DEBUG, QString("-> 
%1").arg(cert.subjectInfo(QSslCertificate::CommonName)));
   }
 
   // what sucks more, this or goto?
@@ -473,6 +279,8 @@
   } while (false);
 
   pendingRequests--;
+
+  ssuLog->print(LOG_DEBUG, QString("Request finished, pending requests: 
%1").arg(pendingRequests));
   if (pendingRequests == 0)
     emit done();
 }
@@ -483,6 +291,8 @@
   QString ssuCaCertificate, ssuRegisterUrl;
   QString username, domainName;
 
+  SsuLog *ssuLog = SsuLog::instance();
+
   // Username can include also domain, (user@domain), separate those
   if (usernameDomain.contains('@')) {
       // separate domain/username and set domain
@@ -509,7 +319,7 @@
   } else
     ssuRegisterUrl = settings->value("register-url").toString();
 
-  QString IMEI = deviceUid();
+  QString IMEI = deviceInfo.deviceUid();
   if (IMEI == ""){
     setError("No valid UID available for your device. For phones: is your 
modem online?");
     return;
@@ -534,7 +344,7 @@
 
   QUrl form;
   form.addQueryItem("protocolVersion", SSU_PROTOCOL_VERSION);
-  form.addQueryItem("deviceModel", deviceModel());
+  form.addQueryItem("deviceModel", deviceInfo.deviceModel());
   if (!domain().isEmpty()){
     form.addQueryItem("domain", domain());
   }
@@ -553,7 +363,7 @@
     // clear header, the other request bits are reusable
     request.setHeader(QNetworkRequest::ContentTypeHeader, 0);
     request.setUrl(homeUrl + "/authorized_keys");
-    qDebug() << "sending request to " << request.url();
+    ssuLog->print(LOG_DEBUG, QString("Trying to get SSH keys from 
%1").arg(request.url().toString()));
     pendingRequests++;
     manager->get(request);
   }
@@ -606,13 +416,19 @@
   errorFlag = true;
   errorString = errorMessage;
 
+  SsuLog *ssuLog = SsuLog::instance();
+
+  // dump error message to systemd journal for easier debugging
+  ssuLog->print(LOG_WARNING, errorMessage);
+
   // assume that we don't even need to wait for other pending requests,
-  // and just die. This is only relevant for CLI, which well exit after done()
+  // and just die. This is only relevant for CLI, which will exit after done()
   emit done();
 }
 
 void Ssu::setFlavour(QString flavour){
   settings->setValue("flavour", flavour);
+  settings->sync();
   emit flavourChanged();
 }
 
@@ -621,6 +437,7 @@
     settings->setValue("rndRelease", release);
   else
     settings->setValue("release", release);
+  settings->sync();
 }
 
 void Ssu::setDomain(QString domain){
@@ -655,7 +472,9 @@
 void Ssu::updateCredentials(bool force){
   errorFlag = false;
 
-  if (deviceUid() == ""){
+  SsuLog *ssuLog = SsuLog::instance();
+
+  if (deviceInfo.deviceUid() == ""){
     setError("No valid UID available for your device. For phones: is your 
modem online?");
     return;
   }
@@ -682,12 +501,14 @@
   }
 
   if (!force){
-    // skip updating if the last update was less than a day ago
+    // skip updating if the last update was less than 30 minutes ago
     QDateTime now = QDateTime::currentDateTime();
 
     if (settings->contains("lastCredentialsUpdate")){
       QDateTime last = settings->value("lastCredentialsUpdate").toDateTime();
-      if (last >= now.addDays(-1)){
+      if (last >= now.addSecs(-1800)){
+        ssuLog->print(LOG_DEBUG, QString("Skipping credentials update, last 
update was at %1")
+                     .arg(last.toString()));
         emit done();
         return;
       }
@@ -710,9 +531,10 @@
   sslConfiguration.setLocalCertificate(certificate);
 
   QNetworkRequest request;
-  request.setUrl(QUrl(ssuCredentialsUrl.arg(deviceUid())));
+  request.setUrl(QUrl(ssuCredentialsUrl.arg(deviceInfo.deviceUid())));
 
-  qDebug() << request.url();
+  ssuLog->print(LOG_DEBUG, QString("Sending credential update request to %1")
+               .arg(request.url().toString()));
   request.setSslConfiguration(sslConfiguration);
 
   pendingRequests++;
@@ -730,6 +552,7 @@
   settings->setValue("privateKey", "");
   settings->setValue("certificate", "");
   settings->setValue("registered", false);
+  settings->sync();
   emit registrationStatusChanged();
 }
 
--- libssu/ssu.h
+++ libssu/ssu.h
@@ -16,11 +16,14 @@
 
 #include <QtXml/QDomDocument>
 
+#include <ssudeviceinfo.h>
+#include <ssusettings.h>
+
 class Ssu: public QObject {
     Q_OBJECT
 
   public:
-    Ssu();
+    Ssu(QString fallbackLog="/tmp/ssu.log");
     /**
      * Find a username/password pair for the given scope
      * @return a QPair with username and password, or an empty QPair if scope 
is invalid
@@ -41,19 +44,6 @@
      */
     QString credentialsUrl(QString scope);
     /**
-     * Try to find the device family for the system this is running on
-     */
-    Q_INVOKABLE QString deviceFamily();
-    /**
-     * Try to find out ond what kind of system this is running
-     */
-    Q_INVOKABLE QString deviceModel();
-    /**
-     * Calculate the device ID used in SSU requests
-     * @return QSystemDeviceInfo::imei(), if available, or 
QSystemDeviceInfo::uniqueDeviceID()
-     */
-    Q_INVOKABLE QString deviceUid();
-    /**
      * Returns if the last operation was successful
      * @retval true last operation was successful
      * @retval false last operation failed, you should check lastError() for 
details
@@ -121,13 +111,20 @@
      */
     Q_INVOKABLE bool useSslVerify();
 
+
+    // compat stuff, might go away when refactoring is finished
+    Q_INVOKABLE QString deviceFamily(){ return deviceInfo.deviceFamily(); };
+    Q_INVOKABLE QString deviceModel(){ return deviceInfo.deviceModel(); };
+    Q_INVOKABLE QString deviceUid(){ return deviceInfo.deviceUid(); };
+
   private:
-    QString errorString;
-    QString cachedModel, cachedFamily;
+    QString errorString, fallbackLogPath;
     bool errorFlag;
     QNetworkAccessManager *manager;
     int pendingRequests;
-    QSettings *settings, *repoSettings, *boardMappings;
+    QSettings *repoSettings;
+    SsuSettings *settings;
+    SsuDeviceInfo deviceInfo;
     bool registerDevice(QDomDocument *response);
     bool setCredentials(QDomDocument *response);
     bool verifyResponse(QDomDocument *response);
--- libssu/ssudeviceinfo.cpp
+++ libssu/ssudeviceinfo.cpp
@@ -0,0 +1,156 @@
+/**
+ * @file ssudeviceinfo.cpp
+ * @copyright 2013 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#include <QSystemDeviceInfo>
+#include <QTextStream>
+#include <QDir>
+
+#include "ssudeviceinfo.h"
+
+#include "../constants.h"
+
+QTM_USE_NAMESPACE
+
+SsuDeviceInfo::SsuDeviceInfo(): QObject(){
+
+    boardMappings = new SsuSettings(SSU_BOARD_MAPPING_CONFIGURATION, 
SSU_BOARD_MAPPING_CONFIGURATION_DIR);
+}
+
+QString SsuDeviceInfo::deviceFamily(){
+  QString model = deviceModel();
+
+  if (!cachedFamily.isEmpty())
+    return cachedFamily;
+
+  cachedFamily = "UNKNOWN";
+
+  if (boardMappings->contains("variants/" + model)) {
+    model = boardMappings->value("variants/" + model).toString();
+    cachedVariant = model;
+  }
+
+  if (boardMappings->contains(model + "/family"))
+    cachedFamily = boardMappings->value(model + "/family").toString();
+
+  return cachedFamily;
+}
+
+QString SsuDeviceInfo::deviceVariant(){
+  if (!cachedVariant.isEmpty())
+    return cachedVariant;
+
+  cachedVariant = "";
+
+  if (boardMappings->contains("variants/" + deviceModel())) {
+    cachedVariant = boardMappings->value("variants/" + 
deviceModel()).toString();
+  }
+
+  return cachedVariant;
+}
+
+QString SsuDeviceInfo::deviceModel(){
+  QDir dir;
+  QFile procCpuinfo;
+  QStringList keys;
+
+  if (!cachedModel.isEmpty())
+    return cachedModel;
+
+  boardMappings->beginGroup("file.exists");
+  keys = boardMappings->allKeys();
+
+  // check if the device can be identified by testing for a file
+  foreach (const QString &key, keys){
+    QString value = boardMappings->value(key).toString();
+    if (dir.exists(value)){
+      cachedModel = key;
+      break;
+    }
+  }
+  boardMappings->endGroup();
+  if (!cachedModel.isEmpty()) return cachedModel;
+
+  // check if the QSystemInfo model is useful
+  QSystemDeviceInfo devInfo;
+  QString model = devInfo.model();
+  boardMappings->beginGroup("systeminfo.equals");
+  keys = boardMappings->allKeys();
+  foreach (const QString &key, keys){
+    QString value = boardMappings->value(key).toString();
+    if (model == value){
+      cachedModel = key;
+      break;
+    }
+  }
+  boardMappings->endGroup();
+  if (!cachedModel.isEmpty()) return cachedModel;
+
+  // check if the device can be identified by a string in /proc/cpuinfo
+  procCpuinfo.setFileName("/proc/cpuinfo");
+  procCpuinfo.open(QIODevice::ReadOnly | QIODevice::Text);
+  if (procCpuinfo.isOpen()){
+    QTextStream in(&procCpuinfo);
+    QString cpuinfo = in.readAll();
+    boardMappings->beginGroup("cpuinfo.contains");
+    keys = boardMappings->allKeys();
+
+    foreach (const QString &key, keys){
+      QString value = boardMappings->value(key).toString();
+      if (cpuinfo.contains(value)){
+        cachedModel = key;
+        break;
+      }
+    }
+    boardMappings->endGroup();
+  }
+  if (!cachedModel.isEmpty()) return cachedModel;
+
+
+  // check if there's a match on arch ofr generic fallback. This probably
+  // only makes sense for x86
+  boardMappings->beginGroup("arch.equals");
+  keys = boardMappings->allKeys();
+
+  QSettings settings(SSU_CONFIGURATION, QSettings::IniFormat);
+  foreach (const QString &key, keys){
+    QString value = boardMappings->value(key).toString();
+    if (settings.value("arch").toString() == value){
+      cachedModel = key;
+      break;
+    }
+  }
+  boardMappings->endGroup();
+  if (cachedModel.isEmpty()) cachedModel = "UNKNOWN";
+
+  return cachedModel;
+}
+
+QString SsuDeviceInfo::deviceUid(){
+  QString IMEI;
+  QSystemDeviceInfo devInfo;
+
+  IMEI = devInfo.imei();
+
+  // this might not be completely unique (or might change on reflash), but 
works for now
+  if (IMEI == ""){
+      IMEI = devInfo.uniqueDeviceID();
+  }
+
+  return IMEI;
+}
+
+bool SsuDeviceInfo::getValue(const QString& key, QString& value){
+  if (boardMappings->contains(deviceVariant()+"/"+key)){
+    value = boardMappings->value(deviceVariant()+"/"+key).toString();
+    return true;
+  }
+  else if (boardMappings->contains(deviceModel()+"/"+key)){
+    value = boardMappings->value(deviceModel()+"/"+key).toString();
+    return true;
+  }
+  return false;
+}
--- libssu/ssudeviceinfo.h
+++ libssu/ssudeviceinfo.h
@@ -0,0 +1,45 @@
+/**
+ * @file ssudeviceinfo.h
+ * @copyright 2013 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#ifndef _SSUDEVICEINFO_H
+#define _SSUDEVICEINFO_H
+
+#include <QObject>
+#include <QSettings>
+
+#include <ssusettings.h>
+
+class SsuDeviceInfo: public QObject {
+    Q_OBJECT
+
+  public:
+    SsuDeviceInfo();
+    /**
+     * Try to find the device family for the system this is running on
+     */
+    Q_INVOKABLE QString deviceFamily();
+    /**
+     * Try to find the device variant for the system this is running on.
+     */
+    Q_INVOKABLE QString deviceVariant();
+    /**
+     * Try to find out ond what kind of system this is running
+     */
+    Q_INVOKABLE QString deviceModel();
+    /**
+     * Calculate the device ID used in SSU requests
+     * @return QSystemDeviceInfo::imei(), if available, or 
QSystemDeviceInfo::uniqueDeviceID()
+     */
+    Q_INVOKABLE QString deviceUid();
+
+    bool getValue(const QString& key, QString& value);
+
+  private:
+    SsuSettings *boardMappings;
+    QString cachedFamily, cachedModel, cachedVariant;
+};
+#endif
--- libssu/ssulog.cpp
+++ libssu/ssulog.cpp
@@ -0,0 +1,37 @@
+/**
+ * @file ssulog.cpp
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#include <QFile>
+#include <QTextStream>
+
+#include "ssulog.h"
+
+SsuLog *SsuLog::ssuLog = 0;
+
+SsuLog *SsuLog::instance(){
+  if (!ssuLog){
+    ssuLog = new SsuLog();
+    ssuLog->fallbackLogPath = "/tmp/ssu.log";
+  }
+
+  return ssuLog;
+}
+
+void SsuLog::print(int priority, QString message){
+  QByteArray ba = message.toUtf8();
+  const char *ca = ba.constData();
+
+  if (sd_journal_print(LOG_INFO, "ssu: %s", ca) < 0 && fallbackLogPath != ""){
+    QFile logfile;
+    QTextStream logstream;
+    logfile.setFileName(fallbackLogPath);
+    logfile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append);
+    logstream.setDevice(&logfile);
+    logstream << message << "\n";
+    logstream.flush();
+  }
+}
--- libssu/ssulog.h
+++ libssu/ssulog.h
@@ -0,0 +1,33 @@
+/**
+ * @file ssulog.h
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#ifndef _SSULOG_H
+#define _SSULOG_H
+
+#include <QObject>
+
+#include <systemd/sd-journal.h>
+
+class SsuLog {
+
+  public:
+    static SsuLog *instance();
+    /**
+     * Print a message to systemds journal, or to a text log file, if a 
fallback is defined
+     */
+    void print(int priority, QString message);
+
+  private:
+    SsuLog() {};
+    SsuLog(const SsuLog &); // hide copy constructor
+
+    static SsuLog *ssuLog;
+    QString fallbackLogPath;
+};
+
+
+#endif
--- libssu/ssusettings.cpp
+++ libssu/ssusettings.cpp
@@ -0,0 +1,175 @@
+/**
+ * @file ssusettings.cpp
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#include <QStringList>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QDateTime>
+
+#include "ssusettings.h"
+#include "ssulog.h"
+
+SsuSettings::SsuSettings(): QSettings(){
+
+}
+
+SsuSettings::SsuSettings(const QString &fileName, Format format, QObject 
*parent):
+  QSettings(fileName, format, parent){
+}
+
+SsuSettings::SsuSettings(const QString &fileName, Format format, const QString 
&defaultFileName, QObject *parent):
+  QSettings(fileName, format, parent){
+  defaultSettingsFile = defaultFileName;
+  upgrade();
+}
+
+SsuSettings::SsuSettings(const QString &fileName, const QString 
&settingsDirectory, QObject *parent):
+  QSettings(fileName, QSettings::IniFormat, parent){
+  settingsd = settingsDirectory;
+  merge();
+}
+
+void SsuSettings::merge(){
+  if (settingsd == "")
+    return;
+
+  bool skipMerge = true;
+
+  SsuLog *ssuLog = SsuLog::instance();
+
+  QDirIterator it(settingsd, QDir::AllEntries|QDir::NoDot|QDir::NoDotDot, 
QDirIterator::FollowSymlinks);
+  QStringList settingsFiles;
+
+  QFileInfo oldSettingsInfo(fileName());
+
+  while (it.hasNext()){
+    QString f = it.next();
+
+    settingsFiles.append(it.filePath());
+
+    QFileInfo info(it.filePath());
+    if (info.lastModified() >= oldSettingsInfo.lastModified())
+      skipMerge = false;
+  }
+
+  if (skipMerge){
+    ssuLog->print(LOG_DEBUG, QString("Configuration file is newer than all 
config.d files, skipping merge"));
+    return;
+  }
+
+  settingsFiles.sort();
+
+  foreach (const QString &settingsFile, settingsFiles){
+    QSettings settings(settingsFile, QSettings::IniFormat);
+    QStringList groups = settings.childGroups();
+
+    ssuLog->print(LOG_DEBUG, QString("Merging %1 into %2")
+                  .arg(settingsFile)
+                  .arg(fileName()));
+
+    foreach (const QString &group, groups){
+      beginGroup(group);
+      settings.beginGroup(group);
+
+      QStringList keys = settings.allKeys();
+      foreach (const QString &key, keys){
+        setValue(key, settings.value(key));
+      }
+
+      settings.endGroup();
+      endGroup();
+    }
+    sync();
+  }
+}
+
+void SsuSettings::upgrade(){
+  int configVersion=0;
+  int defaultConfigVersion=0;
+
+  SsuLog *ssuLog = SsuLog::instance();
+
+  if (defaultSettingsFile == "")
+    return;
+
+  QSettings defaultSettings(defaultSettingsFile, QSettings::IniFormat);
+
+  if (contains("configVersion"))
+    configVersion = value("configVersion").toInt();
+  if (defaultSettings.contains("configVersion"))
+    defaultConfigVersion = defaultSettings.value("configVersion").toInt();
+
+  if (configVersion < defaultConfigVersion){
+    ssuLog->print(LOG_DEBUG, QString("Configuration is outdated, updating from 
%1 to %2")
+                 .arg(configVersion)
+                 .arg(defaultConfigVersion));
+
+    for (int i=configVersion+1;i<=defaultConfigVersion;i++){
+      QStringList defaultKeys;
+      QString currentSection = QString("%1/").arg(i);
+
+      ssuLog->print(LOG_DEBUG, QString("Processing configuration version 
%1").arg(i));
+      defaultSettings.beginGroup(currentSection);
+      defaultKeys = defaultSettings.allKeys();
+      defaultSettings.endGroup();
+      foreach (const QString &key, defaultKeys){
+        // Default keys support both commands and new keys
+        if (key.compare("cmd-remove", Qt::CaseSensitive) == 0){
+          // Remove keys listed in value as string list
+          QStringList oldKeys = defaultSettings.value(currentSection + 
key).toStringList();
+          foreach (const QString &oldKey, oldKeys){
+            if (contains(oldKey)){
+              remove(oldKey);
+              ssuLog->print(LOG_DEBUG, QString("Removing old key: 
%1").arg(oldKey));
+            }
+          }
+        } else if (!contains(key)){
+          // Add new keys..
+          setValue(key, defaultSettings.value(currentSection + key));
+          ssuLog->print(LOG_DEBUG, QString("Adding key: %1").arg(key));
+        } else {
+          // ... or update the ones where default values has changed.
+          QVariant oldValue;
+
+          // check if an old value exists in an older configuration version
+          for (int j=i-1;j>0;j--){
+            if (defaultSettings.contains(QString("%1/").arg(j)+key)){
+              oldValue = defaultSettings.value(QString("%1/").arg(j)+key);
+              break;
+            }
+          }
+
+          // skip updating if there is no old value, since we can't check if 
the
+          // default value has changed
+          if (oldValue.isNull())
+            continue;
+
+          QVariant newValue = defaultSettings.value(currentSection + key);
+          if (oldValue == newValue){
+            // old and new value match, no need to do anything, apart from 
beating the
+            // person who added a useless key
+            continue;
+          } else {
+            // default value has changed, so check if the configuration is 
still
+            // using the old default value...
+            QVariant currentValue = value(key);
+            // testcase: handles properly default update of thing with changed 
value in ssu.ini?
+            if (currentValue == oldValue){
+              // ...and update the key if it does
+              setValue(key, newValue);
+              ssuLog->print(LOG_DEBUG, QString("Updating %1 from %2 to %3")
+                           .arg(key)
+                           .arg(currentValue.toString())
+                           .arg(newValue.toString()));
+            }
+          }
+        }
+      }
+      setValue("configVersion", i);
+    }
+  }
+}
--- libssu/ssusettings.h
+++ libssu/ssusettings.h
@@ -0,0 +1,37 @@
+/**
+ * @file ssusettings.h
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#ifndef _SSUSETTINGS_H
+#define _SSUSETTINGS_H
+
+#include <QSettings>
+
+class SsuSettings: public QSettings {
+    Q_OBJECT
+
+  public:
+    SsuSettings();
+    SsuSettings(const QString &fileName, Format format, QObject *parent=0);
+    /**
+     * Initialize the settings object with a defaults settings file, resulting 
in
+     * update to the configuration file if needed
+     */
+    SsuSettings(const QString &fileName, Format format, const QString 
&defaultFileName, QObject *parent=0);
+    /**
+     * Initialize the settings object from a settings.d structure, if needed. 
Only INI
+     * style settings are supported in this mode.
+     */
+    SsuSettings(const QString &fileName, const QString &settingsDirectory, 
QObject *parent=0);
+
+  private:
+    QString defaultSettingsFile, settingsd;
+    void merge();
+    void upgrade();
+
+};
+
+#endif
--- libssu/ssuvariables.cpp
+++ libssu/ssuvariables.cpp
@@ -0,0 +1,104 @@
+/**
+ * @file ssuvariables.cpp
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#include <QStringList>
+#include <QRegExp>
+#include <QStringRef>
+
+#include "ssuvariables.h"
+
+SsuVariables::SsuVariables(): QObject() {
+
+}
+
+void SsuVariables::resolveSection(QSettings *settings, QString section, 
QHash<QString, QString> *storageHash){
+  QStringList repoVariables;
+
+  settings->beginGroup(section);
+  repoVariables = settings->allKeys();
+  foreach (const QString &key, repoVariables){
+    storageHash->insert(key, settings->value(key).toString());
+  }
+  settings->endGroup();
+}
+
+QString SsuVariables::resolveString(QString pattern, QHash<QString, QString> 
*variables){
+  QRegExp regex("%\\\([^%]*\\\)", Qt::CaseSensitive, QRegExp::RegExp2);
+  regex.setMinimal(true);
+
+  int pos = 0;
+  while ((pos = regex.indexIn(pattern, pos)) != -1){
+    QString match = regex.cap(0);
+
+    if (match.contains(":")){
+      // variable is special, resolve before replacing
+      QString variable = resolveVariable(match, variables);
+      pattern.replace(match, variable);
+      pos += variable.length();
+    } else {
+      // look up variable name in hashmap, and replace it with stored value,
+      // if found, or ""
+      QString variableName = match;
+      variableName.remove(0,2);
+      variableName.chop(1);
+      if (variables->contains(variableName)){
+        pattern.replace(match, variables->value(variableName));
+        pos += variables->value(variableName).length();
+      } else
+        pattern.replace(match, "");
+    }
+  }
+
+  // check if string still contains variables, and recurse
+  if (regex.indexIn(pattern, 0) != -1)
+    pattern = resolveString(pattern, variables);
+
+  return pattern;
+}
+
+QString SsuVariables::resolveVariable(QString variable, QHash<QString, 
QString> *variables){
+  QString variableValue = "";
+
+  if (variable.endsWith(")"))
+    variable.chop(1);
+  if (variable.startsWith("%("))
+    variable.remove(0,2);
+
+  // hunt for your colon
+  int magic = variable.indexOf(":");
+
+  // seems you misplaced your colon
+  if (magic == -1) return variable;
+
+  QStringRef variableName(&variable, 0, magic);
+  QStringRef variableSub(&variable, magic + 2, variable.length() - magic - 2);
+
+  // Fill in variable value for later tests, if it exists
+  if (variables->contains(variableName.toString()))
+    variableValue = variables->value(variableName.toString());
+
+  // find the operator who's after your colon
+  QChar op;
+  if (variable.length() >= magic +1)
+    op = variable.at(magic + 1);
+
+  switch(op.toAscii()){
+    case '-':
+      // substitute default value if variable is empty
+      if (variableValue == "")
+        return variableSub.toString();
+      break;
+    case '+':
+      // substitute default value if variable is not empty
+      if (variableValue != "")
+        return variableSub.toString();
+      break;
+  }
+
+  // no proper substitution found -> return default value
+  return variableValue;
+}
--- libssu/ssuvariables.h
+++ libssu/ssuvariables.h
@@ -0,0 +1,36 @@
+/**
+ * @file ssuvariables.h
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#ifndef _SSUVARIABLES_H
+#define _SSUVARIABLES_H
+
+#include <QObject>
+#include <QSettings>
+#include <QHash>
+
+class SsuVariables: public QObject {
+    Q_OBJECT
+
+  public:
+    SsuVariables();
+    /**
+     * Look up all variables in the specified configuration file section,
+     * run them through the variable expander, and add them to the supplied
+     * QHash
+     */
+    void resolveSection(QSettings *settings, QString section, QHash<QString, 
QString> *storageHash);
+    /**
+     * Resolve a whole string, containing several variables. Variables inside 
variables are allowed
+     */
+    QString resolveString(QString pattern, QHash<QString, QString> *variables);
+    /**
+     * Resolve variables; variable can be passed as %(var) or var
+     */
+    QString resolveVariable(QString variable, QHash<QString, QString> 
*variables);
+};
+
+#endif
--- repos.ini
+++ repos.ini
@@ -16,6 +16,13 @@
 # %(adaptation)    The device specific adaptation, for example 'n900' or 
'n950-n9'
 #
 #
+# Variables may contain other variables. Resolving is done recursively from the
+# innermost variable.
+#
+# Basic variable substitution is supported:
+# %(foo:+bar) -- expands to "" if foo is set, bar otherwise
+# %(foo:-bar) -- expands to %(foo) if foo is set, bar otherwise
+#
 # Repository lookup will happen based on the 'repo' parameter in repository
 # URLs. For RnD repositories order will be <flavour> -> rnd -> all, for
 # release repositories release -> all.
--- rndssucli/rndssucli.cpp
+++ rndssucli/rndssucli.cpp
@@ -6,6 +6,9 @@
  */
 
 #include <QCoreApplication>
+
+#include <termios.h>
+
 #include "rndssucli.h"
 
 RndSsuCli::RndSsuCli(): QObject(){
@@ -28,48 +31,132 @@
 }
 
 
-void RndSsuCli::run(){
+void RndSsuCli::optFlavour(QString newFlavour){
   QTextStream qout(stdout);
 
-  QStringList arguments = QCoreApplication::arguments();
+  if (newFlavour != ""){
+    qout << "Changing flavour from " << ssu.flavour()
+         << " to " << newFlavour << endl;
+    ssu.setFlavour(newFlavour);
+  } else
+    qout << "Device flavour is currently: " << ssu.flavour() << endl;
 
+  QCoreApplication::exit(0);
+}
 
-  if (arguments.count() != 2){
-    usage();
-    return;
+void RndSsuCli::optRegister(){
+  /*
+   * register a new device
+   */
+
+  QString username, password;
+  QTextStream qin(stdin);
+  QTextStream qout(stdout);
+
+  struct termios termNew, termOld;
+
+  qout << "Username: " << flush;
+  username = qin.readLine();
+
+  tcgetattr(STDIN_FILENO, &termNew);
+  termOld = termNew;
+  termNew.c_lflag &= ~ECHO;
+  if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &termNew) == -1)
+    qout << "WARNING: Unable to disable echo on your terminal, password will 
echo!" << endl;
+
+  qout << "Password: " << flush;
+  password = qin.readLine();
+
+  tcsetattr(STDIN_FILENO, TCSANOW, &termOld);
+
+  ssu.sendRegistration(username, password);
+}
+
+void RndSsuCli::optResolve(){
+  /*
+   * resolve URL and print
+   * TODO: arguments
+   */
+
+  QString repo;
+  bool rndRepo=false;
+/*
+  repo = arguments.at(2);
+
+  if (arguments.count() >= 3){
+    //qout << (arguments.at(3).compare("false")||arguments.at(3).compare("0"));
+    qout << (arguments.at(3).compare("false"));
   }
 
-  if (arguments.at(1) == "register"){
-    QString username, password;
-    QTextStream qin(stdin);
-
-    qout << "Username: " << flush;
-    username = qin.readLine();
-    qout << "Password: " << flush;
-    password = qin.readLine();
-
-    ssu.sendRegistration(username, password);
-  } else if (arguments.at(1) == "update"){
-    if (!ssu.isRegistered()){
+  qout << ssu.repoUrl(arguments.at(2));
+  QCoreApplication::exit(1);
+*/
+
+  QCoreApplication::exit(1);
+}
+
+void RndSsuCli::optStatus(){
+  QTextStream qout(stdout);
+
+  /*
+   * print device information and registration status
+   */
+  qout << "Device registration status: "
+       << (ssu.isRegistered() ? "registered" : "not registered") << endl;
+  qout << "Device family: " << ssu.deviceFamily() << endl;
+  qout << "Device model: " << ssu.deviceModel() << endl;
+  qout << "Device UID: " << ssu.deviceUid() << endl;
+
+  QCoreApplication::exit(0);
+}
+
+void RndSsuCli::optUpdate(bool force){
+  QTextStream qout(stdout);
+  /*
+   * update the credentials
+   * optional argument: -f
+   */
+  if (!ssu.isRegistered()){
       qout << "Device is not registered, can't update credentials" << endl;
       QCoreApplication::exit(1);
-    } else {
-      ssu.updateCredentials();
-    }
-  } else if (arguments.at(1) == "status"){
-    qout << "Device registration status: "
-         << (ssu.isRegistered() ? "registered" : "not registered") << endl;
-    qout << "Device family: " << ssu.deviceFamily() << endl;
-    qout << "Device model: " << ssu.deviceModel() << endl;
-    qout << "Device UID: " << ssu.deviceUid() << endl;
-    QCoreApplication::exit(1);
   } else
+    ssu.updateCredentials(force);
+
+  QCoreApplication::exit(0);
+}
+
+void RndSsuCli::run(){
+  QTextStream qout(stdout);
+
+  QStringList arguments = QCoreApplication::arguments();
+
+  // make sure there's a first argument to parse
+  if (arguments.count() < 2){
     usage();
+    return;
+  }
 
+  if (arguments.at(1) == "register" && arguments.count() == 2){
+    optRegister();
+  } else if (arguments.at(1) == "flavour" &&
+             (arguments.count() == 2 || arguments.count() == 3)){
+    if (arguments.count() == 2) optFlavour();
+    else optFlavour(arguments.at(2));
+  } else if (arguments.at(1) == "update" &&
+             (arguments.count() == 2 || arguments.count() == 3)){
+    if (arguments.count() == 3 && arguments.at(2) == "-f")
+      optUpdate(true);
+    else optUpdate(false);
+  } else if (arguments.at(1) == "resolve"){
+    optResolve();
+  } else if (arguments.at(1) == "status" && arguments.count() == 2){
+    optStatus();
+  } else
+    usage();
 }
 
 void RndSsuCli::usage(){
   QTextStream qout(stdout);
-  qout << "Usage: rndssu register|update|status" << endl;
+  qout << "Usage: rndssu flavour [flavour]|register|update [-f]|status" << 
endl;
   QCoreApplication::exit(1);
 }
--- rndssucli/rndssucli.h
+++ rndssucli/rndssucli.h
@@ -27,6 +27,12 @@
     Ssu ssu;
     QSettings settings;
     void usage();
+    void optFlavour(QString newFlavour="");
+    void optRegister();
+    void optResolve();
+    void optStatus();
+    void optUpdate(bool force=false);
+
 
   private slots:
     void handleResponse();
--- rpm/ssu.changes
+++ rpm/ssu.changes
@@ -1,3 +1,23 @@
+* Fri Mar 22 2013 Bernd Wachter <[email protected]> - 0.25
+- Add variable substitution
+
+* Thu Mar 21 2013 Bernd Wachter <[email protected]> - 0.24
+- Add support for arbitrary variables in flavour groups
+- Split off device info and settings functionality
+- Add support for board-mappings.d
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.23
+- Add force option to rndssu update
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.22
+- Add systemd logging to core library
+- Move zypper plugin logging to core library
+- Fix bug where resolverplugin does not return on credentials update
+
+* Mon Mar 11 2013 Bernd Wachter <[email protected]> - 0.21
+- Reduce time before credentials get refreshed
+- Add logging for url resolver
+
 * Tue Jan 29 2013 Bernd Wachter <[email protected]> - 0.20
 - Move ssu resolver log to systemd journal, with fallback in /tmp/ssu.log
 
--- rpm/ssu.spec
+++ rpm/ssu.spec
@@ -1,9 +1,9 @@
 Name: ssu
-Version: 0.20
+Version: 0.25
 Release: 1
 Summary: SSU enabler for RND
 Group: System/Base
-License: Proprietary
+License: GPLv2
 Source0: %{name}-%{version}.tar.gz
 URL: https://github.com/nemomobile/ssu
 BuildRequires: pkgconfig(QtCore)
@@ -93,7 +93,7 @@
 
 
 %prep
-%setup -q
+%setup -q -n %{name}-%{version}
 
 
 %build
--- ssuurlresolver/ssuurlresolver.cpp
+++ ssuurlresolver/ssuurlresolver.cpp
@@ -9,6 +9,7 @@
 #include <systemd/sd-journal.h>
 
 #include "ssuurlresolver.h"
+#include "ssulog.h"
 
 SsuUrlResolver::SsuUrlResolver(): QObject(){
   QObject::connect(this,SIGNAL(done()),
@@ -16,31 +17,41 @@
                    Qt::QueuedConnection);
 }
 
-void SsuUrlResolver::printJournal(int priority, QString message){
-  QByteArray ba = message.toUtf8();
-  const char *ca = ba.constData();
-
-  if (sd_journal_print(LOG_INFO, "ssu: %s", ca) < 0){
-    QFile logfile;
-    QTextStream logstream;
-    logfile.setFileName("/tmp/ssu.log");
-    logfile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append);
-    logstream.setDevice(&logfile);
-    logstream << message << "\n";
-    logstream.flush();
-  }
+bool SsuUrlResolver::writeCredentials(QString filePath, QString 
credentialsScope){
+  QFile credentialsFile(filePath);
+  QPair<QString, QString> credentials = ssu.credentials(credentialsScope);
+  SsuLog *ssuLog = SsuLog::instance();
+
+  if (credentials.first == "" || credentials.second == ""){
+    ssuLog->print(LOG_WARNING, "Returned credentials are empty, skip writing");
+    return false;
+  }
+
+  if (!credentialsFile.open(QIODevice::WriteOnly | QIODevice::Text | 
QIODevice::Truncate)){
+    ssuLog->print(LOG_WARNING, "Unable to open credentials file for writing");
+    return false;
+  }
+
+  QTextStream out(&credentialsFile);
+  out << "[" << ssu.credentialsUrl(credentialsScope) << "]\n";
+  out << "username=" << credentials.first << "\n";
+  out << "password=" << credentials.second << "\n";
+  out.flush();
+  credentialsFile.close();
+  return true;
 }
 
 void SsuUrlResolver::run(){
   QHash<QString, QString> repoParameters;
   QString resolvedUrl, repo;
   bool isRnd = false;
+  SsuLog *ssuLog = SsuLog::instance();
 
   PluginFrame in(std::cin);
 
   if (in.headerEmpty()){
     // FIXME, do something; we need at least repo header
-    printJournal(LOG_WARNING, "Received empty header list. Most likely your 
ssu setup is broken");
+    ssuLog->print(LOG_WARNING, "Received empty header list. Most likely your 
ssu setup is broken");
   }
 
   PluginFrame::HeaderListIterator it;
@@ -77,16 +88,25 @@
   if (!ssu.useSslVerify())
     headerList.append("ssl_verify=no");
 
-  if (isRnd){
+  if (ssu.isRegistered()){
     SignalWait w;
     connect(&ssu, SIGNAL(done()), &w, SLOT(finished()));
     ssu.updateCredentials();
     w.sleep();
-  }
+
+    // error can be found in ssu.log, so just exit
+    // TODO: figure out if there's better eror handling for
+    //       zypper plugins than 'blow up'
+    if (ssu.error()){
+      emit done();
+      return;
+    }
+  } else
+    ssuLog->print(LOG_DEBUG, "Device not registered -- skipping credential 
update");
 
   // TODO: check for credentials scope required for repository; check if the 
file exists;
   //       compare with configuration, and dump credentials to file if 
necessary
-  printJournal(LOG_DEBUG, QString("Requesting credentials for '%1' with RND 
status %2...").arg(repo).arg(isRnd));
+  ssuLog->print(LOG_DEBUG, QString("Requesting credentials for '%1' with RND 
status %2...").arg(repo).arg(isRnd));
   QString credentialsScope = ssu.credentialsScope(repo, isRnd);
   if (!credentialsScope.isEmpty()){
     headerList.append(QString("credentials=%1").arg(credentialsScope));
@@ -94,17 +114,10 @@
     QFileInfo credentialsFileInfo("/etc/zypp/credentials.d/" + 
credentialsScope);
     if (!credentialsFileInfo.exists() ||
         credentialsFileInfo.lastModified() <= ssu.lastCredentialsUpdate()){
-      QFile credentialsFile(credentialsFileInfo.filePath());
-      credentialsFile.open(QIODevice::WriteOnly | QIODevice::Text | 
QIODevice::Truncate);
-      QTextStream out(&credentialsFile);
-      QPair<QString, QString> credentials = ssu.credentials(credentialsScope);
-      out << "[" << ssu.credentialsUrl(credentialsScope) << "]\n";
-      out << "username=" << credentials.first << "\n";
-      out << "password=" << credentials.second << "\n";
-      out.flush();
-      credentialsFile.close();
+      writeCredentials(credentialsFileInfo.filePath(), credentialsScope);
     }
-  }
+  } else
+    ssuLog->print(LOG_DEBUG, "Skipping credential update due to missing 
credentials scope");
 
   if (headerList.isEmpty()){
     resolvedUrl = ssu.repoUrl(repo, isRnd, repoParameters);
@@ -114,7 +127,9 @@
       .arg(headerList.join("&"));
   }
 
-  printJournal(LOG_INFO, QString("%1 resolved to 
%2").arg(repo).arg(resolvedUrl));
+  // TODO, we should bail out here if the configuration specifies that the repo
+  //       is protected, but device is not registered and/or we don't have 
credentials
+  ssuLog->print(LOG_INFO, QString("%1 resolved to 
%2").arg(repo).arg(resolvedUrl));
 
   PluginFrame out("RESOLVEDURL");
   out.setBody(resolvedUrl.toStdString());
--- ssuurlresolver/ssuurlresolver.h
+++ ssuurlresolver/ssuurlresolver.h
@@ -53,6 +53,7 @@
   private:
     Ssu ssu;
     void printJournal(int priority, QString message);
+    bool writeCredentials(QString filePath, QString credentialsScope);
 
   public slots:
     void run();
--- tests/tests.pro
+++ tests/tests.pro
@@ -1,6 +1,6 @@
 TEMPLATE        = subdirs
 CONFIG         += qt ordered coverage debug
-SUBDIRS         = ut_urlresolver
+SUBDIRS         = ut_urlresolver ut_variables
 
 !include( tests.pri ) { error("Unable to find tests include") }
 
--- tests/tests.xml
+++ tests/tests.xml
@@ -8,5 +8,10 @@
         <step expected_result="0">/opt/tests/ssu/ut_urlresolver</step>
       </case>
     </set>
+    <set name="variables" description="Test to determine if variable resolving 
works properly" feature="variables">
+      <case name="ut_variables" type="Functional" description="Variable 
resolver tests" timeout="1000" subfeature="">
+        <step expected_result="0">/opt/tests/ssu/ut_variables</step>
+      </case>
+    </set>
   </suite>
 </testdefinition>
--- tests/ut_variables
+++ tests/ut_variables
+(directory)
--- tests/ut_variables/main.cpp
+++ tests/ut_variables/main.cpp
@@ -0,0 +1,19 @@
+/**
+ * @file main.cpp
+ * @copyright 2012 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2012
+ */
+
+#include <QtTest/QtTest>
+
+#include "variablestest.h"
+
+int main(int argc, char **argv){
+  VariablesTest variablesTest;
+
+  if (QTest::qExec(&variablesTest, argc, argv))
+    return 1;
+
+  return 0;
+}
--- tests/ut_variables/ut_variables.pro
+++ tests/ut_variables/ut_variables.pro
@@ -0,0 +1,17 @@
+HEADERS = variablestest.h
+SOURCES = main.cpp \
+        variablestest.cpp
+TEMPLATE = app
+TARGET = ut_variables
+LIBS += -lssu
+CONFIG -= app_bundle
+CONFIG += console qtestlib
+QT -= gui
+QT += network testlib
+
+!include( ../tests.pri ) { error("Unable to find tests include") }
+
+unix:target.path = $${PREFIX}/$$TESTS_PATH
+INSTALLS += target
+
+!include( ../../buildpath.pri ) { error("Unable to find build path 
specification") }
--- tests/ut_variables/variablestest.cpp
+++ tests/ut_variables/variablestest.cpp
@@ -0,0 +1,63 @@
+/**
+ * @file variablestest.cpp
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#include "variablestest.h"
+
+void VariablesTest::initTestCase(){
+  variables.insert("packagesDomain", "packages.example.com");
+  variables.insert("releaseDomain", "releases.example.com");
+  variables.insert("rndProtocol", "https");
+  variables.insert("release", "devel");
+  variables.insert("arch", "armv8");
+  variables.insert("flavourName", "flavour");
+
+  urls.insert("http://%(packagesDomain)/releases/%(release)/jolla/%(arch)/",
+              "http://packages.example.com/releases/devel/jolla/armv8/";);
+  
urls.insert("%(rndProtocol)://%(releaseDomain)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              
"https://releases.example.com/nemo/devel-flavour/platform/armv8/";);
+  // test missing operator, which should fall back to just variable value
+  
urls.insert("%(rndProtocol)://%(unsetDomain:)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              "https:///nemo/devel-flavour/platform/armv8/";);
+  
urls.insert("%(rndProtocol)://%(releaseDomain:)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              
"https://releases.example.com/nemo/devel-flavour/platform/armv8/";);
+  
urls.insert("%(rndProtocol)://%(releaseDomain:unset.example.com)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              
"https://releases.example.com/nemo/devel-flavour/platform/armv8/";);
+  // check if :- works
+  
urls.insert("%(rndProtocol)://%(releaseDomain:-unset.example.com)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              
"https://releases.example.com/nemo/devel-flavour/platform/armv8/";);
+  
urls.insert("%(rndProtocol)://%(unsetDomain:-unset.example.com)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              "https://unset.example.com/nemo/devel-flavour/platform/armv8/";);
+  // test with empty replacement pattern
+  
urls.insert("%(rndProtocol)://%(unsetDomain:-)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              "https:///nemo/devel-flavour/platform/armv8/";);
+  // check if :+ works
+  // substitution of variable with set.example.com
+  
urls.insert("%(rndProtocol)://%(releaseDomain:+set.example.com)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              "https://set.example.com/nemo/devel-flavour/platform/armv8/";);
+  // substitution of variable with variable + /set
+  
urls.insert("%(rndProtocol)://%(releaseDomain:+%(releaseDomain)/set)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              
"https://releases.example.com/set/nemo/devel-flavour/platform/armv8/";);
+  // substitution of variable with empty variable + /set -- should substitute 
to ""
+  
urls.insert("%(rndProtocol)://%(unsetDomain:+%(unsetDomain)/set)/nemo/%(release)-%(flavourName)/platform/%(arch)/",
+              "https:///nemo/devel-flavour/platform/armv8/";);
+
+}
+
+void VariablesTest::cleanupTestCase(){
+
+}
+
+void VariablesTest::checkResolveString(){
+  QHashIterator<QString, QString> i(urls);
+
+  while (i.hasNext()){
+    i.next();
+    QString result = var.resolveString(i.key(), &variables);
+    qDebug() << i.key() << " resolved to " << result;
+    QCOMPARE(result, i.value());
+  }
+}
--- tests/ut_variables/variablestest.h
+++ tests/ut_variables/variablestest.h
@@ -0,0 +1,33 @@
+/**
+ * @file variablestest.h
+ * @copyright 2013 Jolla Ltd.
+ * @author Bernd Wachter <[email protected]>
+ * @date 2013
+ */
+
+#ifndef _VARIABLESTEST_H
+#define _VARIABLESTEST_H
+
+#include <QObject>
+#include <QtTest/QtTest>
+#include <QHash>
+
+#include <ssu.h>
+#include <ssuvariables.h>
+
+class VariablesTest: public QObject {
+    Q_OBJECT
+
+  private slots:
+    void initTestCase();
+    void cleanupTestCase();
+    void checkResolveString();
+
+
+  private:
+    Ssu ssu;
+    SsuVariables var;
+    QHash <QString, QString> variables, urls;
+};
+
+#endif



Reply via email to