Skip to content

Commit

Permalink
First attempt at cloudinit support
Browse files Browse the repository at this point in the history
Experimental
  • Loading branch information
maxnet committed Nov 18, 2021
1 parent 2e5cc75 commit 8f9fbcf
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 32 deletions.
81 changes: 75 additions & 6 deletions OptionsPopup.qml
Expand Up @@ -23,6 +23,10 @@ Popup {
property string config
property string cmdline
property string firstrun
property string cloudinit
property string cloudinitrun
property string cloudinitwrite
property string cloudinitnetwork

// background of title
Rectangle {
Expand Down Expand Up @@ -496,12 +500,29 @@ Popup {
function escapeshellarg(arg) {
return "'"+arg.replace(/'/g, "\\'")+"'"
}
function addCloudInit(s) {
cloudinit += s+"\n"
}
function addCloudInitWriteFile(name, content, perms) {
cloudinitwrite += "- encoding: b64\n"
cloudinitwrite += " content: "+Qt.btoa(content)+"\n"
cloudinitwrite += " owner: root:root\n"
cloudinitwrite += " path: "+name+"\n"
cloudinitwrite += " permissions: '"+perms+"'\n"
}
function addCloudInitRun(cmd) {
cloudinitrun += "- "+cmd+"\n"
}

function applySettings()
{
cmdline = ""
config = ""
firstrun = ""
cloudinit = ""
cloudinitrun = ""
cloudinitwrite = ""
cloudinitnetwork = ""

if (chkOverscan.checked) {
addConfig("disable_overscan=1")
Expand All @@ -510,15 +531,31 @@ Popup {
addFirstRun("CURRENT_HOSTNAME=`cat /etc/hostname | tr -d \" \\t\\n\\r\"`")
addFirstRun("echo "+fieldHostname.text+" >/etc/hostname")
addFirstRun("sed -i \"s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t"+fieldHostname.text+"/g\" /etc/hosts")

addCloudInit("hostname: "+fieldHostname.text)
addCloudInit("manage_etc_hosts: true")
addCloudInit("packages:")
addCloudInit("- avahi-daemon")
addCloudInit("")
}
if (chkSSH.checked) {
// First user may not be called 'pi' on all distributions, so look username up
addFirstRun("FIRSTUSER=`getent passwd 1000 | cut -d: -f1`");
addFirstRun("FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6`")

addCloudInit("users:")
addCloudInit("- name: pi")
addCloudInit(" groups: users,adm,dialout,audio,netdev,video,plugdev,sudo")
addCloudInit(" shell: /bin/bash")

if (radioPasswordAuthentication.checked) {
var cryptedPassword = fieldUserPassword.alreadyCrypted ? fieldUserPassword.text : imageWriter.crypt(fieldUserPassword.text)
addFirstRun("echo \"$FIRSTUSER:\""+escapeshellarg(cryptedPassword)+" | chpasswd -e")

addCloudInit(" lock_passwd: false")
addCloudInit(" passwd: "+cryptedPassword)
addCloudInit("")
addCloudInit("ssh_pwauth: true")
}
if (radioPubKeyAuthentication.checked) {
var pubkey = fieldPublicKey.text.replace(/\n/g, "")
Expand All @@ -527,8 +564,14 @@ Popup {
addFirstRun("install -o \"$FIRSTUSER\" -m 600 <(echo \""+pubkey+"\") \"$FIRSTUSERHOME/.ssh/authorized_keys\"")
}
addFirstRun("echo 'PasswordAuthentication no' >>/etc/ssh/sshd_config")

addCloudInit(" lock_passwd: true")
addCloudInit(" ssh_authorized_keys:")
addCloudInit(" - "+pubkey)
addCloudInit(" sudo: ALL=(ALL) NOPASSWD:ALL")
}
addFirstRun("systemctl enable ssh")
addCloudInit("")
}
if (chkWifi.checked) {
var wpaconfig = "country="+fieldWifiCountry.editText+"\n"
Expand All @@ -549,22 +592,39 @@ Popup {
addFirstRun("for filename in /var/lib/systemd/rfkill/*:wlan ; do")
addFirstRun(" echo 0 > $filename")
addFirstRun("done")

cloudinitnetwork = "version: 2\n"
cloudinitnetwork += "wifis:\n"
cloudinitnetwork += " renderer: networkd\n"
cloudinitnetwork += " wlan0:\n"
cloudinitnetwork += " dhcp4: true\n"
cloudinitnetwork += " optional: true\n"
cloudinitnetwork += " access-points:\n"
cloudinitnetwork += " "+fieldWifiSSID.text+":\n"
cloudinitnetwork += " password: \""+cryptedPsk+"\"\n"
}
if (chkLocale.checked) {
if (chkSkipFirstUse) {
addFirstRun("rm -f /etc/xdg/autostart/piwiz.desktop")
addCloudInitRun("rm -f /etc/xdg/autostart/piwiz.desktop")
}

var kbdconfig = "XKBMODEL=\"pc105\"\n"
kbdconfig += "XKBLAYOUT=\""+fieldKeyboardLayout.text+"\"\n"
kbdconfig += "XKBVARIANT=\"\"\n"
kbdconfig += "XKBOPTIONS=\"\"\n"

addFirstRun("rm -f /etc/localtime")
addFirstRun("echo \""+fieldTimezone.editText+"\" >/etc/timezone")
addFirstRun("dpkg-reconfigure -f noninteractive tzdata")
addFirstRun("cat >/etc/default/keyboard <<'KBEOF'")
addFirstRun("XKBMODEL=\"pc105\"")
addFirstRun("XKBLAYOUT=\""+fieldKeyboardLayout.text+"\"")
addFirstRun("XKBVARIANT=\"\"")
addFirstRun("XKBOPTIONS=\"\"")
addFirstRun(kbdconfig)
addFirstRun("KBEOF")
addFirstRun("dpkg-reconfigure -f noninteractive keyboard-configuration")

addCloudInit("timezone: "+fieldTimezone.editText)
addCloudInitWriteFile("/etc/default/keyboard", kbdconfig, '0644')
addCloudInitRun("dpkg-reconfigure -f noninteractive keyboard-configuration || true")
}

if (firstrun.length) {
Expand All @@ -574,10 +634,19 @@ Popup {
addFirstRun("exit 0")
/* using systemd.run_success_action=none does not seem to have desired effect
systemd then stays at "reached target kernel command line", so use reboot instead */
addCmdline("systemd.run=/boot/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target")
//addCmdline("systemd.run=/boot/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target")
// cmdline changing moved to DownloadThread::_customizeImage()
}

if (cloudinitwrite !== "") {
addCloudInit("write_files:\n"+cloudinitwrite+"\n")
}

if (cloudinitrun !== "") {
addCloudInit("runcmd:\n"+cloudinitrun+"\n")
}

imageWriter.setImageCustomization(config, cmdline, firstrun)
imageWriter.setImageCustomization(config, cmdline, firstrun, cloudinit, cloudinitnetwork)
}

function saveSettings()
Expand Down
98 changes: 83 additions & 15 deletions downloadthread.cpp
Expand Up @@ -829,11 +829,14 @@ qint64 DownloadThread::_sectorsWritten()
return -1;
}

void DownloadThread::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun)
void DownloadThread::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun, const QByteArray &cloudinit, const QByteArray &cloudInitNetwork, const QByteArray &initFormat)
{
_config = config;
_cmdline = cmdline;
_firstrun = firstrun;
_cloudinit = cloudinit;
_cloudinitNetwork = cloudInitNetwork;
_initFormat = initFormat;
}

bool DownloadThread::_customizeImage()
Expand Down Expand Up @@ -987,20 +990,6 @@ bool DownloadThread::_customizeImage()

emit preparationStatusUpdate(tr("Customizing image"));

if (!_firstrun.isEmpty())
{
QFile f(folder+"/firstrun.sh");
if (f.open(f.WriteOnly) && f.write(_firstrun) == _firstrun.length())
{
f.close();
}
else
{
emit error(tr("Error creating firstrun.sh on FAT partition"));
return false;
}
}

if (!_config.isEmpty())
{
auto configItems = _config.split('\n');
Expand Down Expand Up @@ -1041,6 +1030,85 @@ bool DownloadThread::_customizeImage()
}
}

if (_initFormat == "auto")
{
/* Do an attempt at auto-detecting what customization format a custom
image provided by the user supports */
QByteArray issue;
QFile fi(folder+"/issue.txt");
if (fi.exists() && fi.open(fi.ReadOnly))
{
issue = fi.readAll();
fi.close();
}

if (QFile::exists(folder+"/user-data"))
{
/* If we have user-data file on FAT partition, then it must be cloudinit */
_initFormat = "cloudinit";
qDebug() << "user-data found on FAT partition. Assuming cloudinit support";
}
else if (issue.contains("pi-gen"))
{
/* If issue.txt mentions pi-gen, and there is no user-data file assume
* it is a RPI OS flavor, and use the old systemd unit firstrun script stuff */
_initFormat = "systemd";
qDebug() << "using firstrun script invoked by systemd customization method";
}
else
{
/* Fallback to writing cloudinit file, as it does not hurt having one
* Will just have no customization if OS does not support it */
_initFormat = "cloudinit";
qDebug() << "Unknown what customization method image supports. Falling back to cloudinit";
}
}

if (!_firstrun.isEmpty() && _initFormat == "systemd")
{
QFile f(folder+"/firstrun.sh");
if (f.open(f.WriteOnly) && f.write(_firstrun) == _firstrun.length())
{
f.close();
}
else
{
emit error(tr("Error creating firstrun.sh on FAT partition"));
return false;
}

_cmdline += " systemd.run=/boot/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target";
}

if (!_cloudinit.isEmpty() && _initFormat == "cloudinit")
{
_cloudinit = "#cloud-config\n"+_cloudinit;
QFile f(folder+"/user-data");
if (f.open(f.WriteOnly) && f.write(_cloudinit) == _cloudinit.length())
{
f.close();
}
else
{
emit error(tr("Error creating user-data cloudinit file on FAT partition"));
return false;
}
}

if (!_cloudinitNetwork.isEmpty() && _initFormat == "cloudinit")
{
QFile f(folder+"/network-config");
if (f.open(f.WriteOnly) && f.write(_cloudinitNetwork) == _cloudinitNetwork.length())
{
f.close();
}
else
{
emit error(tr("Error creating network-config cloudinit file on FAT partition"));
return false;
}
}

if (!_cmdline.isEmpty())
{
QByteArray cmdline;
Expand Down
4 changes: 2 additions & 2 deletions downloadthread.h
Expand Up @@ -112,7 +112,7 @@ class DownloadThread : public QThread
/*
* Enable image customization
*/
void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun);
void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun, const QByteArray &cloudinit, const QByteArray &cloudinitNetwork, const QByteArray &initFormat);

/*
* Thread safe download progress query functions
Expand Down Expand Up @@ -164,7 +164,7 @@ class DownloadThread : public QThread
curl_off_t _startOffset;
std::atomic<std::uint64_t> _lastDlTotal, _lastDlNow, _verifyTotal, _lastVerifyNow, _bytesWritten;
qint64 _sectorsStart;
QByteArray _url, _useragent, _buf, _filename, _lastError, _expectedHash, _config, _cmdline, _firstrun;
QByteArray _url, _useragent, _buf, _filename, _lastError, _expectedHash, _config, _cmdline, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat;
char *_firstBlock;
size_t _firstBlockSize;
static QByteArray _proxy;
Expand Down
1 change: 1 addition & 0 deletions icons/ic_cog_40px.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 13 additions & 3 deletions imagewriter.cpp
Expand Up @@ -138,7 +138,7 @@ void ImageWriter::setEngine(QQmlApplicationEngine *engine)
}

/* Set URL to download from */
void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen, QByteArray expectedHash, bool multifilesinzip, QString parentcategory, QString osname)
void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen, QByteArray expectedHash, bool multifilesinzip, QString parentcategory, QString osname, QByteArray initFormat)
{
_src = url;
_downloadLen = downloadLen;
Expand All @@ -147,11 +147,13 @@ void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen,
_multipleFilesInZip = multifilesinzip;
_parentCategory = parentcategory;
_osName = osname;
_initFormat = (initFormat == "none") ? "" : initFormat;

if (!_downloadLen && url.isLocalFile())
{
QFileInfo fi(url.toLocalFile());
_downloadLen = fi.size();
_initFormat = "auto";
}
}

Expand Down Expand Up @@ -238,7 +240,7 @@ void ImageWriter::startWrite()
connect(_thread, SIGNAL(preparationStatusUpdate(QString)), SLOT(onPreparationStatusUpdate(QString)));
_thread->setVerifyEnabled(_verifyEnabled);
_thread->setUserAgent(QString("Mozilla/5.0 rpi-imager/%1").arg(constantVersion()).toUtf8());
_thread->setImageCustomization(_config, _cmdline, _firstrun);
_thread->setImageCustomization(_config, _cmdline, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat);

if (!_expectedHash.isEmpty() && _cachedFileHash != _expectedHash && _cachingEnabled)
{
Expand Down Expand Up @@ -941,15 +943,18 @@ void ImageWriter::setSetting(const QString &key, const QVariant &value)
_settings.sync();
}

void ImageWriter::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun)
void ImageWriter::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun, const QByteArray &cloudinit, const QByteArray &cloudinitNetwork)
{
_config = config;
_cmdline = cmdline;
_firstrun = firstrun;
_cloudinit = cloudinit;
_cloudinitNetwork = cloudinitNetwork;

qDebug() << "Custom config.txt entries:" << config;
qDebug() << "Custom cmdline.txt entries:" << cmdline;
qDebug() << "Custom firstuse.sh:" << firstrun;
qDebug() << "Cloudinit:" << cloudinit;
}

QString ImageWriter::crypt(const QByteArray &password)
Expand Down Expand Up @@ -1022,6 +1027,11 @@ bool ImageWriter::hasSavedCustomizationSettings()
return result;
}

bool ImageWriter::imageSupportsCustomization()
{
return !_initFormat.isEmpty();
}

void MountUtilsLog(std::string msg) {
Q_UNUSED(msg)
//qDebug() << "mountutils:" << msg.c_str();
Expand Down
7 changes: 4 additions & 3 deletions imagewriter.h
Expand Up @@ -29,7 +29,7 @@ class ImageWriter : public QObject
void setEngine(QQmlApplicationEngine *engine);

/* Set URL to download from, and if known download length and uncompressed length */
Q_INVOKABLE void setSrc(const QUrl &url, quint64 downloadLen = 0, quint64 extrLen = 0, QByteArray expectedHash = "", bool multifilesinzip = false, QString parentcategory = "", QString osname = "");
Q_INVOKABLE void setSrc(const QUrl &url, quint64 downloadLen = 0, quint64 extrLen = 0, QByteArray expectedHash = "", bool multifilesinzip = false, QString parentcategory = "", QString osname = "", QByteArray initFormat = "");

/* Set device to write to */
Q_INVOKABLE void setDst(const QString &device, quint64 deviceSize = 0);
Expand Down Expand Up @@ -102,11 +102,12 @@ class ImageWriter : public QObject

Q_INVOKABLE bool getBoolSetting(const QString &key);
Q_INVOKABLE void setSetting(const QString &key, const QVariant &value);
Q_INVOKABLE void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun);
Q_INVOKABLE void setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun, const QByteArray &cloudinit, const QByteArray &cloudinitNetwork);
Q_INVOKABLE void setSavedCustomizationSettings(const QVariantMap &map);
Q_INVOKABLE QVariantMap getSavedCustomizationSettings();
Q_INVOKABLE void clearSavedCustomizationSettings();
Q_INVOKABLE bool hasSavedCustomizationSettings();
Q_INVOKABLE bool imageSupportsCustomization();

Q_INVOKABLE QString crypt(const QByteArray &password);
Q_INVOKABLE QString pbkdf2(const QByteArray &psk, const QByteArray &ssid);
Expand Down Expand Up @@ -143,7 +144,7 @@ protected slots:
protected:
QUrl _src, _repo;
QString _dst, _cacheFileName, _parentCategory, _osName;
QByteArray _expectedHash, _cachedFileHash, _cmdline, _config, _firstrun;
QByteArray _expectedHash, _cachedFileHash, _cmdline, _config, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat;
quint64 _downloadLen, _extrLen, _devLen, _dlnow, _verifynow;
DriveListModel _drivelist;
QQmlApplicationEngine *_engine;
Expand Down

0 comments on commit 8f9fbcf

Please sign in to comment.