Rebrand project to PiSCSI (#1016)

* Rebrand project to PiSCSI
- rascsi ->piscsi
- rasctl -> scsictl
- rasdump -> scsidump
- ras* -> piscsi* (rasutil -> piscsi_util, etc.)

* Refined the formatting and wording of the app startup banner
* Kept some references to rascsi and rasctl where backwards compatibility is concerned
* Point to the new github repo URL

Co-authored-by: nucleogenic <nr@nucleogenic.com>
Co-authored-by: Uwe Seimet <Uwe.Seimet@seimet.de>
This commit is contained in:
Daniel Markstedt
2022-12-05 09:58:23 -08:00
committed by GitHub
parent 12068cafb8
commit 52c2aa474f
274 changed files with 2341 additions and 2380 deletions
+67
View File
@@ -0,0 +1,67 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "shared/log.h"
#include "generated/piscsi_interface.pb.h"
#include "command_context.h"
#include <iostream>
using namespace std;
using namespace piscsi_interface;
void CommandContext::Cleanup()
{
if (fd != -1) {
close(fd);
fd = -1;
}
}
bool CommandContext::ReturnLocalizedError(LocalizationKey key, const string& arg1, const string& arg2,
const string& arg3) const
{
return ReturnLocalizedError(key, NO_ERROR_CODE, arg1, arg2, arg3);
}
bool CommandContext::ReturnLocalizedError(LocalizationKey key, PbErrorCode error_code, const string& arg1,
const string& arg2, const string& arg3) const
{
// For the logfile always use English
LOGERROR("%s", localizer.Localize(key, "en", arg1, arg2, arg3).c_str())
return ReturnStatus(false, localizer.Localize(key, locale, arg1, arg2, arg3), error_code, false);
}
bool CommandContext::ReturnStatus(bool status, const string& msg, PbErrorCode error_code, bool log) const
{
// Do not log twice if logging has already been done in the localized error handling above
if (log && !status && !msg.empty()) {
LOGERROR("%s", msg.c_str())
}
if (fd == -1) {
if (!msg.empty()) {
if (status) {
cerr << "Error: " << msg << endl;
}
else {
cout << msg << endl;
}
}
}
else {
PbResult result;
result.set_status(status);
result.set_error_code(error_code);
result.set_msg(msg);
serializer.SerializeMessage(fd, result);
}
return status;
}
+45
View File
@@ -0,0 +1,45 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "generated/piscsi_interface.pb.h"
#include "localizer.h"
#include "shared/protobuf_serializer.h"
#include <string>
using namespace std;
using namespace piscsi_interface;
class CommandContext
{
const ProtobufSerializer serializer;
const Localizer localizer;
string locale;
int fd;
public:
CommandContext(const std::string& s = "", int f = -1) : locale(s), fd(f) {}
~CommandContext() = default;
void Cleanup();
const ProtobufSerializer& GetSerializer() const { return serializer; }
int GetFd() const { return fd; }
void SetFd(int f) { fd = f; }
bool IsValid() const { return fd != -1; }
bool ReturnLocalizedError(LocalizationKey, const string& = "", const string& = "", const string& = "") const;
bool ReturnLocalizedError(LocalizationKey, PbErrorCode, const string& = "", const string& = "", const string& = "") const;
bool ReturnStatus(bool = true, const string& = "", PbErrorCode = PbErrorCode::NO_ERROR_CODE, bool = true) const;
};
+213
View File
@@ -0,0 +1,213 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "localizer.h"
#include <cassert>
#include <string>
#include <algorithm>
#include <regex>
using namespace std;
Localizer::Localizer()
{
Add(LocalizationKey::ERROR_AUTHENTICATION, "en", "Authentication failed");
Add(LocalizationKey::ERROR_AUTHENTICATION, "de", "Authentifizierung fehlgeschlagen");
Add(LocalizationKey::ERROR_AUTHENTICATION, "sv", "Autentiseringen misslyckades");
Add(LocalizationKey::ERROR_AUTHENTICATION, "fr", "Authentification éronnée");
Add(LocalizationKey::ERROR_AUTHENTICATION, "es", "Fallo de autentificación");
Add(LocalizationKey::ERROR_OPERATION, "en", "Unknown operation");
Add(LocalizationKey::ERROR_OPERATION, "de", "Unbekannte Operation");
Add(LocalizationKey::ERROR_OPERATION, "sv", "Okänd operation");
Add(LocalizationKey::ERROR_OPERATION, "fr", "Opération inconnue");
Add(LocalizationKey::ERROR_OPERATION, "es", "Operación desconocida");
Add(LocalizationKey::ERROR_LOG_LEVEL, "en", "Invalid log level %1");
Add(LocalizationKey::ERROR_LOG_LEVEL, "de", "Ungültiger Log-Level %1");
Add(LocalizationKey::ERROR_LOG_LEVEL, "sv", "Ogiltig loggnivå %1");
Add(LocalizationKey::ERROR_LOG_LEVEL, "fr", "Niveau de journalisation invalide %1");
Add(LocalizationKey::ERROR_LOG_LEVEL, "es", "Nivel de registro %1 no válido");
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "en", "Missing device ID");
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "de", "Fehlende Geräte-ID");
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "sv", "Enhetens id saknas");
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "fr", "ID de périphérique manquante");
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "es", "Falta el ID del dispositivo");
Add(LocalizationKey::ERROR_MISSING_FILENAME, "en", "Missing filename");
Add(LocalizationKey::ERROR_MISSING_FILENAME, "de", "Fehlender Dateiname");
Add(LocalizationKey::ERROR_MISSING_FILENAME, "sv", "Filnamn saknas");
Add(LocalizationKey::ERROR_MISSING_FILENAME, "fr", "Nom de fichier manquant");
Add(LocalizationKey::ERROR_MISSING_FILENAME, "es", "Falta el nombre del archivo");
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "en", "Device type %1 requires a filename");
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "de", "Gerätetyp %1 erfordert einen Dateinamen");
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "sv", "Enhetstypen %1 kräver ett filnamn");
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "es", "El tipo de dispositivo %1 requiere un nombre de archivo");
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "en", "Image file '%1' is already being used by ID %2, unit %3");
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "de", "Image-Datei '%1' wird bereits von ID %2, Einheit %3 benutzt");
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "sv", "Skivbildsfilen '%1' används redan av id %2, enhetsnummer %3");
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "fr", "Le fichier d'image '%1' est déjà utilisé par l'ID %2, unité %3");
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "es", "El archivo de imagen '%1' ya está siendo utilizado por el ID %2, unidad %3");
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "en", "Can't create image file info for '%1'");
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "de", "Image-Datei-Information für '%1' kann nicht erzeugt werden");
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "sv", "Kunde ej skapa skivbildsfilsinfo för '%1'");
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "es", "No se puede crear información de archivo de imagen para '%1'");
Add(LocalizationKey::ERROR_RESERVED_ID, "en", "Device ID %1 is reserved");
Add(LocalizationKey::ERROR_RESERVED_ID, "de", "Geräte-ID %1 ist reserviert");
Add(LocalizationKey::ERROR_RESERVED_ID, "sv", "Enhets-id %1 är reserverat");
Add(LocalizationKey::ERROR_RESERVED_ID, "fr", "ID de périphérique %1 réservée");
Add(LocalizationKey::ERROR_RESERVED_ID, "es", "El ID de dispositivo %1 está reservado");
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "en", "Command for non-existing ID %1");
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "de", "Kommando für nicht existente ID %1");
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "sv", "Kommando för id %1 som ej existerar");
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "fr", "Commande pour ID %1 non-existant");
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "es", "Comando para ID %1 no existente");
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "en", "Command for non-existing ID %1, unit %2");
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "de", "Kommando für nicht existente ID %1, Einheit %2");
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "sv", "Kommando för id %1, enhetsnummer %2 som ej existerar");
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "fr", "Command pour ID %1, unité %2 non-existant");
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "es", "Comando para ID %1 inexistente, unidad %2");
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "en", "Unknown device type %1");
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "de", "Unbekannter Gerätetyp %1");
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "sv", "Obekant enhetstyp: %1");
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "fr", "Type de périphérique inconnu %1");
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "es", "Tipo de dispositivo desconocido %1");
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "en", "Device type required for unknown extension of file '%1'");
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "de", "Gerätetyp erforderlich für unbekannte Extension der Datei '%1'");
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "sv", "Man måste ange enhetstyp för obekant filändelse '%1'");
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "fr", "Type de périphérique requis pour extension inconnue du fichier '%1'");
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "es", "Tipo de dispositivo requerido para la extensión desconocida del archivo '%1'");
Add(LocalizationKey::ERROR_DUPLICATE_ID, "en", "Duplicate ID %1, unit %2");
Add(LocalizationKey::ERROR_DUPLICATE_ID, "de", "Doppelte ID %1, Einheit %2");
Add(LocalizationKey::ERROR_DUPLICATE_ID, "sv", "Duplikat id %1, enhetsnummer %2");
Add(LocalizationKey::ERROR_DUPLICATE_ID, "fr", "ID %1, unité %2 dupliquée");
Add(LocalizationKey::ERROR_DUPLICATE_ID, "es", "ID duplicado %1, unidad %2");
Add(LocalizationKey::ERROR_DETACH, "en", "Couldn't detach device");
Add(LocalizationKey::ERROR_DETACH, "de", "Geräte konnte nicht entfernt werden");
Add(LocalizationKey::ERROR_DETACH, "sv", "Kunde ej koppla ifrån enheten");
Add(LocalizationKey::ERROR_DETACH, "es", "No se ha podido desconectar el dispositivo");
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "en", "Existing medium must first be ejected");
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "de", "Das vorhandene Medium muss erst ausgeworfen werden");
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "sv", "Nuvarande skiva måste utmatas först");
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "fr", "Media déjà existant doit d'abord être éjecté");
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "es", "El medio existente debe ser expulsado primero");
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "en", "Once set the device name cannot be changed anymore");
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "de", "Ein bereits gesetzter Gerätename kann nicht mehr geändert werden");
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "sv", "Enhetsnamn kan ej ändras efter att ha fastställts en gång");
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "fr", "Une fois défini, le nom de périphérique ne peut plus être changé");
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "es", "Una vez establecido el nombre del dispositivo ya no se puede cambiar");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "en", "Missing shutdown mode");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "de", "Fehlender Shutdown-Modus");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "sv", "Avstängningsläge saknas");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "fr", "Mode d'extinction manquant");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "es", "Falta el modo de apagado");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "en", "Invalid shutdown mode '%1'");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "de", "Ungültiger Shutdown-Modus '%1'");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "sv", "Ogiltigt avstängsningsläge: '%1'");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "fr", "Mode d'extinction invalide '%1'");
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "es", "Modo de apagado inválido '%1'");
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "en", "Missing root permission for shutdown or reboot");
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "de", "Fehlende Root-Berechtigung für Shutdown oder Neustart");
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "sv", "Saknar root-rättigheter för att kunna stänga av eller starta om systemet");
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "fr", "Permissions root manquantes pour extinction ou redémarrage");
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "es", "Falta el permiso de root para el apagado o el reinicio");
Add(LocalizationKey::ERROR_FILE_OPEN, "en", "Invalid or non-existing file '%1'");
Add(LocalizationKey::ERROR_FILE_OPEN, "de", "Ungültige oder fehlende Datei '%1'");
Add(LocalizationKey::ERROR_FILE_OPEN, "sv", "Ogiltig eller saknad fil '%1'");
Add(LocalizationKey::ERROR_FILE_OPEN, "fr", "Fichier invalide ou non-existant '%1'");
Add(LocalizationKey::ERROR_FILE_OPEN, "es", "Archivo inválido o inexistente '%1'");
Add(LocalizationKey::ERROR_BLOCK_SIZE, "en", "Invalid block size %1 bytes");
Add(LocalizationKey::ERROR_BLOCK_SIZE, "de", "Ungültige Blockgröße %1 Bytes");
Add(LocalizationKey::ERROR_BLOCK_SIZE, "sv", "Ogiltig blockstorlek: %1 byte");
Add(LocalizationKey::ERROR_BLOCK_SIZE, "fr", "Taille de bloc invalide %1 octets");
Add(LocalizationKey::ERROR_BLOCK_SIZE, "es", "Tamaño de bloque inválido %1 bytes");
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "en", "Block size for device type %1 is not configurable");
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "de", "Blockgröße für Gerätetyp %1 ist nicht konfigurierbar");
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "sv", "Enhetstypen %1 kan inte använda andra blockstorlekar");
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "fr", "Taille de block pour le type de périphérique %1 non configurable");
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "es", "El tamaño del bloque para el tipo de dispositivo %1 no es configurable");
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "en", "Couldn't create SCSI controller");
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "de", "SCSI-Controller konnte nicht erzeugt werden");
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "sv", "Kunde ej skapa SCSI-gränssnitt");
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "es", "No se ha podido crear el controlador SCSI");
Add(LocalizationKey::ERROR_INVALID_ID, "en", "Invalid device ID %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_ID, "de", "Ungültige Geräte-ID %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_ID, "sv", "Ogiltigt enhets-id %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_ID, "es", "ID de dispositivo inválido %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_LUN, "en", "Invalid LUN %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_LUN, "de", "Ungültige LUN %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_LUN, "sv", "Ogiltigt enhetsnummer %1 (0-%2)");
Add(LocalizationKey::ERROR_INVALID_LUN, "es", "LUN invalido %1 (0-%2)");
Add(LocalizationKey::ERROR_LUN0, "en", "LUN 0 cannot be detached as long as there is still another LUN");
Add(LocalizationKey::ERROR_LUN0, "de", "LUN 0 kann nicht entfernt werden, solange noch eine andere LUN existiert");
Add(LocalizationKey::ERROR_LUN0, "sv", "Enhetsnummer 0 kan ej bli frånkopplat så länge som andra enhetsnummer är anslutna");
Add(LocalizationKey::ERROR_LUN0, "es", "El LUN 0 no se puede desconectar mientras haya otro LUN");
Add(LocalizationKey::ERROR_INITIALIZATION, "en", "Initialization of %1 device, ID %2, LUN %3 failed");
Add(LocalizationKey::ERROR_INITIALIZATION, "de", "Initialisierung von %1-Gerät, ID %2, LUN %3 fehlgeschlagen");
Add(LocalizationKey::ERROR_INITIALIZATION, "sv", "Kunde ej initialisera enheten %1 med id %2 och enhetsnummer %3");
Add(LocalizationKey::ERROR_INITIALIZATION, "es", "La inicialización del dispositivo %1, ID %2, LUN %3 falló");
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "en", "%1 operation denied, %2 isn't stoppable");
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "de", "%1-Operation verweigert, %2 ist nicht stopbar");
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "sv", "Operationen %1 nekades för att %2 inte kan stoppas");
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "es", "%1 operación denegada, %2 no se puede parar");
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "en", "%1 operation denied, %2 isn't removable");
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "de", "%1-Operation verweigert, %2 ist nicht wechselbar");
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "sv", "Operationen %1 nekades för att %2 inte är uttagbar(t)");
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "es", "%1 operación denegada, %2 no es removible");
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "en", "%1 operation denied, %2 isn't protectable");
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "de", "%1-Operation verweigert, %2 ist nicht schützbar");
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "sv", "Operationen %1 nekades för att %2 inte är skyddbar(t)");
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "es", "%1 operación denegada, %2 no es protegible");
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "en", "%1 operation denied, %2 isn't ready");
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "de", "%1-Operation verweigert, %2 ist nicht bereit");
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "sv", "Operationen %1 nekades för att %2 inte är redo");
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "es", "%1 operación denegada, %2 no está listo");
}
void Localizer::Add(LocalizationKey key, const string& locale, string_view value)
{
// Safeguards against empty messages, duplicate entries and unsupported locales
assert(locale.size());
assert(value.size());
assert(supported_languages.find(locale) != supported_languages.end());
assert(localized_messages[locale][key].empty());
localized_messages[locale][key] = value;
}
string Localizer::Localize(LocalizationKey key, const string& locale, const string& arg1, const string& arg2,
const string &arg3) const
{
string locale_lower = locale;
transform(locale_lower.begin(), locale_lower.end(), locale_lower.begin(), ::tolower);
auto it = localized_messages.find(locale_lower);
if (it == localized_messages.end()) {
// Try to fall back to country-indepedent locale (e.g. "en" instead of "en_US")
if (locale_lower.length() > 2) {
it = localized_messages.find(locale_lower.substr(0, 2));
}
if (it == localized_messages.end()) {
it = localized_messages.find("en");
}
}
assert (it != localized_messages.end());
auto messages = it->second;
const auto& m = messages.find(key);
if (m == messages.end()) {
return "Missing localization for enum value " + to_string(static_cast<int>(key));
}
string message = m->second;
message = regex_replace(message, regex("%1"), arg1);
message = regex_replace(message, regex("%2"), arg2);
message = regex_replace(message, regex("%3"), arg3);
return message;
}
+71
View File
@@ -0,0 +1,71 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
// Message localization support. Currently only for messages with up to 3 string parameters.
//
//---------------------------------------------------------------------------
#pragma once
#include <string>
#include <unordered_set>
#include <unordered_map>
using namespace std;
enum class LocalizationKey {
ERROR_AUTHENTICATION,
ERROR_OPERATION,
ERROR_LOG_LEVEL,
ERROR_MISSING_DEVICE_ID,
ERROR_MISSING_FILENAME,
ERROR_DEVICE_MISSING_FILENAME,
ERROR_IMAGE_IN_USE,
ERROR_IMAGE_FILE_INFO,
ERROR_RESERVED_ID,
ERROR_NON_EXISTING_DEVICE,
ERROR_NON_EXISTING_UNIT,
ERROR_UNKNOWN_DEVICE_TYPE,
ERROR_MISSING_DEVICE_TYPE,
ERROR_DUPLICATE_ID,
ERROR_DETACH,
ERROR_EJECT_REQUIRED,
ERROR_DEVICE_NAME_UPDATE,
ERROR_SHUTDOWN_MODE_MISSING,
ERROR_SHUTDOWN_MODE_INVALID,
ERROR_SHUTDOWN_PERMISSION,
ERROR_FILE_OPEN,
ERROR_BLOCK_SIZE,
ERROR_BLOCK_SIZE_NOT_CONFIGURABLE,
ERROR_SCSI_CONTROLLER,
ERROR_INVALID_ID,
ERROR_INVALID_LUN,
ERROR_LUN0,
ERROR_INITIALIZATION,
ERROR_OPERATION_DENIED_STOPPABLE,
ERROR_OPERATION_DENIED_REMOVABLE,
ERROR_OPERATION_DENIED_PROTECTABLE,
ERROR_OPERATION_DENIED_READY
};
class Localizer
{
public:
Localizer();
~Localizer() = default;
string Localize(LocalizationKey, const string&, const string& = "", const string& = "", const string& = "") const;
private:
void Add(LocalizationKey, const string&, string_view);
unordered_map<string, unordered_map<LocalizationKey, string>> localized_messages;
// Supported locales, always lower case
unordered_set<string> supported_languages = { "en", "de", "sv", "fr", "es" };
};
+661
View File
@@ -0,0 +1,661 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Powered by XM6 TypeG Technology.
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) 2020-2022 Contributors to the PiSCSI project
//
//---------------------------------------------------------------------------
#include "shared/config.h"
#include "shared/log.h"
#include "shared/piscsi_util.h"
#include "shared/protobuf_serializer.h"
#include "shared/protobuf_util.h"
#include "shared/piscsi_exceptions.h"
#include "shared/piscsi_version.h"
#include "controllers/controller_manager.h"
#include "controllers/scsi_controller.h"
#include "devices/device_factory.h"
#include "devices/storage_device.h"
#include "hal/gpiobus_factory.h"
#include "hal/gpiobus.h"
#include "hal/systimer.h"
#include "piscsi/piscsi_executor.h"
#include "piscsi/piscsi_core.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include <netinet/in.h>
#include <csignal>
#include <string>
#include <sstream>
#include <iostream>
#include <fstream>
#include <list>
#include <filesystem>
using namespace std;
using namespace filesystem;
using namespace spdlog;
using namespace piscsi_interface;
using namespace piscsi_util;
using namespace protobuf_util;
using namespace scsi_defs;
void Piscsi::Banner(const vector<char *>& args) const
{
cout << piscsi_util::Banner("(Backend Service)");
cout << "Connection type: " << CONNECT_DESC << '\n' << flush;
if ((args.size() > 1 && strcmp(args[1], "-h") == 0) || (args.size() > 1 && strcmp(args[1], "--help") == 0)){
cout << "\nUsage: " << args[0] << " [-idID[:LUN] FILE] ...\n\n";
cout << " ID is SCSI device ID (0-7).\n";
cout << " LUN is the optional logical unit (0-31).\n";
cout << " FILE is a disk image file, \"daynaport\", \"bridge\", \"printer\" or \"services\".\n\n";
cout << " Image type is detected based on file extension if no explicit type is specified.\n";
cout << " hd1 : SCSI-1 HD image (Non-removable generic SCSI-1 HD image)\n";
cout << " hds : SCSI HD image (Non-removable generic SCSI HD image)\n";
cout << " hdr : SCSI HD image (Removable generic HD image)\n";
cout << " hda : SCSI HD image (Apple compatible image)\n";
cout << " hdn : SCSI HD image (NEC compatible image)\n";
cout << " hdi : SCSI HD image (Anex86 HD image)\n";
cout << " nhd : SCSI HD image (T98Next HD image)\n";
cout << " mos : SCSI MO image (MO image)\n";
cout << " iso : SCSI CD image (ISO 9660 image)\n" << flush;
exit(EXIT_SUCCESS);
}
}
bool Piscsi::InitBus() const
{
bus = GPIOBUS_Factory::Create(BUS::mode_e::TARGET);
if (bus == nullptr) {
return false;
}
auto b = bus;
controller_manager = make_shared<ControllerManager>(*b);
auto c = controller_manager;
executor = make_shared<PiscsiExecutor>(piscsi_image, *c);
return true;
}
void Piscsi::Cleanup()
{
executor->DetachAll();
service.Cleanup();
bus->Cleanup();
}
void Piscsi::ReadAccessToken(const string& filename) const
{
struct stat st;
if (stat(filename.c_str(), &st) || !S_ISREG(st.st_mode)) {
throw parser_exception("Can't access token file '" + filename + "'");
}
if (st.st_uid || st.st_gid) {
throw parser_exception("Access token file '" + filename + "' must be owned by root");
}
if (const auto perms = filesystem::status(filename).permissions();
(perms & perms::group_read) != perms::none || (perms & perms::others_read) != perms::none ||
(perms & perms::group_write) != perms::none || (perms & perms::others_write) != perms::none) {
throw parser_exception("Access token file '" + filename + "' must be readable by root only");
}
ifstream token_file(filename);
if (token_file.fail()) {
throw parser_exception("Can't open access token file '" + filename + "'");
}
getline(token_file, access_token);
if (token_file.fail()) {
throw parser_exception("Can't read access token file '" + filename + "'");
}
if (access_token.empty()) {
throw parser_exception("Access token file '" + filename + "' must not be empty");
}
}
void Piscsi::LogDevices(string_view devices) const
{
stringstream ss(devices.data());
string line;
while (getline(ss, line, '\n')) {
LOGINFO("%s", line.c_str())
}
}
PbDeviceType Piscsi::ParseDeviceType(const string& value) const
{
string t = value;
PbDeviceType type;
transform(t.begin(), t.end(), t.begin(), ::toupper);
if (!PbDeviceType_Parse(t, &type)) {
throw parser_exception("Illegal device type '" + value + "'");
}
return type;
}
void Piscsi::TerminationHandler(int signum)
{
Cleanup();
exit(signum);
}
Piscsi::optargs_type Piscsi::ParseArguments(const vector<char *>& args, int& port) const
{
optargs_type optargs;
int block_size = 0;
string name;
opterr = 1;
int opt;
while ((opt = getopt(static_cast<int>(args.size()), args.data(), "-Iib:d:n:p:r:t:z:D:F:L:P:R:C:v")) != -1) {
switch (opt) {
// The following options can not be processed until AFTER
// the 'bus' object is created and configured
case 'i':
case 'I':
case 'd':
case 'D':
case 'R':
case 'n':
case 'r':
case 't':
case 'F':
case 'z':
{
const string optarg_str = optarg == nullptr ? "" : optarg;
optargs.emplace_back(opt, optarg_str);
continue;
}
case 'b': {
if (!GetAsUnsignedInt(optarg, block_size)) {
throw parser_exception("Invalid block size " + string(optarg));
}
continue;
}
case 'L':
current_log_level = optarg;
continue;
case 'p':
if (!GetAsUnsignedInt(optarg, port) || port <= 0 || port > 65535) {
throw parser_exception("Invalid port " + string(optarg) + ", port must be between 1 and 65535");
}
continue;
case 'P':
ReadAccessToken(optarg);
continue;
case 'v':
cout << piscsi_get_version_string() << endl;
exit(0);
case 1:
{
// Encountered filename
const string optarg_str = (optarg == nullptr) ? "" : string(optarg);
optargs.emplace_back(opt, optarg_str);
continue;
}
default:
throw parser_exception("Parser error");
}
if (optopt) {
throw parser_exception("Praser error");
}
}
return optargs;
}
void Piscsi::CreateInitialDevices(const optargs_type& optargs) const
{
PbCommand command;
PbDeviceType type = UNDEFINED;
int block_size = 0;
string name;
string log_level;
string id_and_lun;
const char *locale = setlocale(LC_MESSAGES, "");
if (locale == nullptr || !strcmp(locale, "C")) {
locale = "en";
}
opterr = 1;
for (const auto& [option, value] : optargs) {
switch (option) {
case 'i':
case 'I':
continue;
case 'd':
case 'D':
id_and_lun = value;
continue;
case 'z':
locale = value.c_str();
continue;
case 'F':
if (const string error = piscsi_image.SetDefaultFolder(value); !error.empty()) {
throw parser_exception(error);
}
continue;
case 'R':
int depth;
if (!GetAsUnsignedInt(value, depth)) {
throw parser_exception("Invalid image file scan depth " + value);
}
piscsi_image.SetDepth(depth);
continue;
case 'n':
name = value;
continue;
case 'r':
if (const string error = executor->SetReservedIds(value); !error.empty()) {
throw parser_exception(error);
}
continue;
case 't':
type = ParseDeviceType(value);
continue;
case 1:
// Encountered filename
break;
default:
throw parser_exception("Parser error");
}
PbDeviceDefinition *device = command.add_devices();
if (!id_and_lun.empty()) {
if (const string error = SetIdAndLun(*device, id_and_lun, ScsiController::LUN_MAX); !error.empty()) {
throw parser_exception(error);
}
}
device->set_type(type);
device->set_block_size(block_size);
ParseParameters(*device, value);
SetProductData(*device, name);
type = UNDEFINED;
block_size = 0;
name = "";
id_and_lun = "";
}
// Attach all specified devices
command.set_operation(ATTACH);
if (CommandContext context(locale); !executor->ProcessCmd(context, command)) {
throw parser_exception("Can't execute " + PbOperation_Name(command.operation()));
}
// Display and log the device list
PbServerInfo server_info;
piscsi_response.GetDevices(controller_manager->GetAllDevices(), server_info, piscsi_image.GetDefaultFolder());
const list<PbDevice>& devices = { server_info.devices_info().devices().begin(), server_info.devices_info().devices().end() };
const string device_list = ListDevices(devices);
LogDevices(device_list);
cout << device_list << flush;
}
bool Piscsi::ExecuteCommand(const CommandContext& context, const PbCommand& command)
{
if (!access_token.empty() && access_token != GetParam(command, "token")) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_AUTHENTICATION, UNAUTHORIZED);
}
if (!PbOperation_IsValid(command.operation())) {
LOGERROR("Received unknown command with operation opcode %d", command.operation())
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION, UNKNOWN_OPERATION);
}
LOGTRACE("Received %s command", PbOperation_Name(command.operation()).c_str())
PbResult result;
ProtobufSerializer serializer;
switch(command.operation()) {
case LOG_LEVEL: {
const string log_level = GetParam(command, "level");
if (const bool status = executor->SetLogLevel(log_level); !status) {
context.ReturnLocalizedError(LocalizationKey::ERROR_LOG_LEVEL, log_level);
}
else {
current_log_level = log_level;
context.ReturnStatus();
}
break;
}
case DEFAULT_FOLDER: {
if (const string status = piscsi_image.SetDefaultFolder(GetParam(command, "folder")); !status.empty()) {
context.ReturnStatus(false, status);
}
else {
context.ReturnStatus();
}
break;
}
case DEVICES_INFO: {
piscsi_response.GetDevicesInfo(controller_manager->GetAllDevices(), result, command,
piscsi_image.GetDefaultFolder());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case DEVICE_TYPES_INFO: {
result.set_allocated_device_types_info(piscsi_response.GetDeviceTypesInfo(result).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case SERVER_INFO: {
result.set_allocated_server_info(piscsi_response.GetServerInfo(controller_manager->GetAllDevices(),
result, executor->GetReservedIds(), current_log_level, piscsi_image.GetDefaultFolder(),
GetParam(command, "folder_pattern"), GetParam(command, "file_pattern"),
piscsi_image.GetDepth()).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case VERSION_INFO: {
result.set_allocated_version_info(piscsi_response.GetVersionInfo(result).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case LOG_LEVEL_INFO: {
result.set_allocated_log_level_info(piscsi_response.GetLogLevelInfo(result, current_log_level).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case DEFAULT_IMAGE_FILES_INFO: {
result.set_allocated_image_files_info(piscsi_response.GetAvailableImages(result,
piscsi_image.GetDefaultFolder(), GetParam(command, "folder_pattern"),
GetParam(command, "file_pattern"), piscsi_image.GetDepth()).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case IMAGE_FILE_INFO: {
if (string filename = GetParam(command, "file"); filename.empty()) {
context.ReturnLocalizedError( LocalizationKey::ERROR_MISSING_FILENAME);
}
else {
auto image_file = make_unique<PbImageFile>();
const bool status = piscsi_response.GetImageFile(*image_file.get(), piscsi_image.GetDefaultFolder(), filename);
if (status) {
result.set_status(true);
result.set_allocated_image_file_info(image_file.get());
serializer.SerializeMessage(context.GetFd(), result);
}
else {
context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_FILE_INFO);
}
}
break;
}
case NETWORK_INTERFACES_INFO: {
result.set_allocated_network_interfaces_info(piscsi_response.GetNetworkInterfacesInfo(result).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case MAPPING_INFO: {
result.set_allocated_mapping_info(piscsi_response.GetMappingInfo(result).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case OPERATION_INFO: {
result.set_allocated_operation_info(piscsi_response.GetOperationInfo(result,
piscsi_image.GetDepth()).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case RESERVED_IDS_INFO: {
result.set_allocated_reserved_ids_info(piscsi_response.GetReservedIds(result,
executor->GetReservedIds()).release());
serializer.SerializeMessage(context.GetFd(), result);
break;
}
case SHUT_DOWN: {
if (executor->ShutDown(context, GetParam(command, "mode"))) {
TerminationHandler(0);
}
break;
}
default: {
// Wait until we become idle
const timespec ts = { .tv_sec = 0, .tv_nsec = 500'000'000};
while (active) {
nanosleep(&ts, nullptr);
}
executor->ProcessCmd(context, command);
break;
}
}
return true;
}
int Piscsi::run(const vector<char *>& args)
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
Banner(args);
int port = DEFAULT_PORT;
optargs_type optargs;
try {
optargs = ParseArguments(args, port);
}
catch(const parser_exception& e) {
cerr << "Error: " << e.what() << endl;
return EXIT_FAILURE;
}
// current_log_level may have been updated by ParseArguments()
executor->SetLogLevel(current_log_level);
// Create a thread-safe stdout logger to process the log messages
const auto logger = stdout_color_mt("piscsi stdout logger");
if (!InitBus()) {
cerr << "Error: Can't initialize bus" << endl;
return EXIT_FAILURE;
}
// We need to wait to create the devices until after the bus/controller/etc objects have been created
// TODO Try to resolve dependencies so that this work-around can be removed
try {
CreateInitialDevices(optargs);
}
catch(const parser_exception& e) {
cerr << "Error: " << e.what() << endl;
Cleanup();
return EXIT_FAILURE;
}
if (!service.Init(&ExecuteCommand, port)) {
return EXIT_FAILURE;
}
// Signal handler to detach all devices on a KILL or TERM signal
struct sigaction termination_handler;
termination_handler.sa_handler = TerminationHandler;
sigemptyset(&termination_handler.sa_mask);
termination_handler.sa_flags = 0;
sigaction(SIGINT, &termination_handler, nullptr);
sigaction(SIGTERM, &termination_handler, nullptr);
// Set the affinity to a specific processor core
FixCpu(3);
sched_param schparam;
#ifdef USE_SEL_EVENT_ENABLE
// Scheduling policy setting (highest priority)
schparam.sched_priority = sched_get_priority_max(SCHED_FIFO);
sched_setscheduler(0, SCHED_FIFO, &schparam);
#else
cout << "Note: No PiSCSI hardware support, only client interface calls are supported" << endl;
#endif
// Start execution
service.SetRunning(true);
// Main Loop
while (service.IsRunning()) {
#ifdef USE_SEL_EVENT_ENABLE
// SEL signal polling
if (!bus->PollSelectEvent()) {
// Stop on interrupt
if (errno == EINTR) {
break;
}
continue;
}
// Get the bus
bus->Acquire();
#else
bus->Acquire();
if (!bus->GetSEL()) {
const timespec ts = { .tv_sec = 0, .tv_nsec = 0};
nanosleep(&ts, nullptr);
continue;
}
#endif
// Wait until BSY is released as there is a possibility for the
// initiator to assert it while setting the ID (for up to 3 seconds)
WaitForNotBusy();
// Stop because the bus is busy or another device responded
if (bus->GetBSY() || !bus->GetSEL()) {
continue;
}
int initiator_id = AbstractController::UNKNOWN_INITIATOR_ID;
// The initiator and target ID
const uint8_t id_data = bus->GetDAT();
phase_t phase = phase_t::busfree;
// Identify the responsible controller
auto controller = controller_manager->IdentifyController(id_data);
if (controller != nullptr) {
device_logger.SetIdAndLun(controller->GetTargetId(), -1);
initiator_id = controller->ExtractInitiatorId(id_data);
if (initiator_id != AbstractController::UNKNOWN_INITIATOR_ID) {
device_logger.Trace("++++ Starting processing for initiator ID " + to_string(initiator_id));
}
else {
device_logger.Trace("++++ Starting processing for unknown initiator ID");
}
if (controller->Process(initiator_id) == phase_t::selection) {
phase = phase_t::selection;
}
}
// Return to bus monitoring if the selection phase has not started
if (phase != phase_t::selection) {
continue;
}
// Start target device
active = true;
#if !defined(USE_SEL_EVENT_ENABLE) && defined(__linux__)
// Scheduling policy setting (highest priority)
schparam.sched_priority = sched_get_priority_max(SCHED_FIFO);
sched_setscheduler(0, SCHED_FIFO, &schparam);
#endif
// Loop until the bus is free
while (service.IsRunning()) {
// Target drive
phase = controller->Process(initiator_id);
// End when the bus is free
if (phase == phase_t::busfree) {
break;
}
}
#if !defined(USE_SEL_EVENT_ENABLE) && defined(__linux__)
// Set the scheduling priority back to normal
schparam.sched_priority = 0;
sched_setscheduler(0, SCHED_OTHER, &schparam);
#endif
// End the target travel
active = false;
}
return EXIT_SUCCESS;
}
void Piscsi::WaitForNotBusy() const
{
if (bus->GetBSY()) {
const uint32_t now = SysTimer::GetTimerLow();
// Wait for 3s
while ((SysTimer::GetTimerLow() - now) < 3'000'000) {
bus->Acquire();
if (!bus->GetBSY()) {
break;
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "devices/device_logger.h"
#include "piscsi/command_context.h"
#include "piscsi/piscsi_service.h"
#include "piscsi/piscsi_image.h"
#include "piscsi/piscsi_response.h"
#include "generated/piscsi_interface.pb.h"
#include <vector>
#include <string>
using namespace std;
class BUS;
class ControllerManager;
class PiscsiExecutor;
class Piscsi
{
using optargs_type = vector<pair<int, string>>;
static const int DEFAULT_PORT = 6868;
public:
Piscsi() = default;
~Piscsi() = default;
int run(const vector<char *>&);
private:
void Banner(const vector<char *>&) const;
bool InitBus() const;
static void Cleanup();
void ReadAccessToken(const string&) const;
void LogDevices(string_view) const;
PbDeviceType ParseDeviceType(const string&) const;
static void TerminationHandler(int);
optargs_type ParseArguments(const vector<char *>&, int&) const;
void CreateInitialDevices(const optargs_type&) const;
void WaitForNotBusy() const;
// TODO Should not be static and should be moved to PiscsiService
static bool ExecuteCommand(const CommandContext&, const PbCommand&);
DeviceLogger device_logger;
// A static instance is needed because of the signal handler
static inline shared_ptr<BUS> bus;
// TODO These fields should not be static
static inline PiscsiService service;
static inline PiscsiImage piscsi_image;
const static inline PiscsiResponse piscsi_response;
static inline shared_ptr<ControllerManager> controller_manager;
static inline shared_ptr<PiscsiExecutor> executor;
// Processing flag
static inline volatile bool active;
// Some versions of spdlog do not support get_log_level(), so we have to remember the level
static inline string current_log_level = "info";
static inline string access_token;
};
+779
View File
@@ -0,0 +1,779 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "shared/log.h"
#include "shared/piscsi_util.h"
#include "shared/protobuf_util.h"
#include "shared/piscsi_exceptions.h"
#include "controllers/controller_manager.h"
#include "controllers/scsi_controller.h"
#include "devices/device_logger.h"
#include "devices/device_factory.h"
#include "devices/primary_device.h"
#include "devices/disk.h"
#include "piscsi_service.h"
#include "piscsi_image.h"
#include "localizer.h"
#include "command_context.h"
#include "piscsi_executor.h"
#include <sstream>
using namespace spdlog;
using namespace protobuf_util;
using namespace piscsi_util;
bool PiscsiExecutor::ProcessDeviceCmd(const CommandContext& context, const PbDeviceDefinition& pb_device,
const PbCommand& command, bool dryRun)
{
PrintCommand(command, pb_device, dryRun);
const int id = pb_device.id();
const int lun = pb_device.unit();
if (!ValidateIdAndLun(context, id, lun)) {
return false;
}
const PbOperation operation = command.operation();
// For all commands except ATTACH the device and LUN must exist
if (operation != ATTACH && !VerifyExistingIdAndLun(context, id, lun)) {
return false;
}
auto device = controller_manager.GetDeviceByIdAndLun(id, lun);
if (!ValidateOperationAgainstDevice(context, *device, operation)) {
return false;
}
switch (operation) {
case START:
return Start(*device, dryRun);
case STOP:
return Stop(*device, dryRun);
case ATTACH:
return Attach(context, pb_device, dryRun);
case DETACH:
return Detach(context, device, dryRun);
case INSERT:
return Insert(context, pb_device, device, dryRun);
case EJECT:
return Eject(*device, dryRun);
case PROTECT:
return Protect(*device, dryRun);
case UNPROTECT:
return Unprotect(*device, dryRun);
break;
case CHECK_AUTHENTICATION:
case NO_OPERATION:
// Do nothing, just log
LOGTRACE("Received %s command", PbOperation_Name(operation).c_str())
break;
default:
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION);
}
return true;
}
bool PiscsiExecutor::ProcessCmd(const CommandContext& context, const PbCommand& command)
{
switch (command.operation()) {
case DETACH_ALL:
DetachAll();
return context.ReturnStatus();
case RESERVE_IDS: {
const string ids = GetParam(command, "ids");
if (const string error = SetReservedIds(ids); !error.empty()) {
return context.ReturnStatus(false, error);
}
return context.ReturnStatus();
}
case CREATE_IMAGE:
return piscsi_image.CreateImage(context, command);
case DELETE_IMAGE:
return piscsi_image.DeleteImage(context, command);
case RENAME_IMAGE:
return piscsi_image.RenameImage(context, command);
case COPY_IMAGE:
return piscsi_image.CopyImage(context, command);
case PROTECT_IMAGE:
case UNPROTECT_IMAGE:
return piscsi_image.SetImagePermissions(context, command);
default:
// This is a device-specific command handled below
break;
}
// Remember the list of reserved files, than run the dry run
const auto& reserved_files = StorageDevice::GetReservedFiles();
for (const auto& device : command.devices()) {
if (!ProcessDeviceCmd(context, device, command, true)) {
// Dry run failed, restore the file list
StorageDevice::SetReservedFiles(reserved_files);
return false;
}
}
// Restore the list of reserved files before proceeding
StorageDevice::SetReservedFiles(reserved_files);
if (const string result = ValidateLunSetup(command); !result.empty()) {
return context.ReturnStatus(false, result);
}
for (const auto& device : command.devices()) {
if (!ProcessDeviceCmd(context, device, command, false)) {
return false;
}
}
// ATTACH and DETACH return the device list
if (context.IsValid() && (command.operation() == ATTACH || command.operation() == DETACH)) {
// A new command with an empty device list is required here in order to return data for all devices
PbCommand cmd;
PbResult result;
piscsi_response.GetDevicesInfo(controller_manager.GetAllDevices(), result, cmd,
piscsi_image.GetDefaultFolder());
serializer.SerializeMessage(context.GetFd(), result);
return true;
}
return context.ReturnStatus();
}
bool PiscsiExecutor::SetLogLevel(const string& log_level) const
{
int id = -1;
int lun = -1;
string level = log_level;
if (size_t separator_pos = log_level.find(COMPONENT_SEPARATOR); separator_pos != string::npos) {
level = log_level.substr(0, separator_pos);
const string l = log_level.substr(separator_pos + 1);
separator_pos = l.find(COMPONENT_SEPARATOR);
if (separator_pos != string::npos) {
const string error = ProcessId(l, ScsiController::LUN_MAX, id, lun);
if (!error.empty()) {
LOGWARN("Invalid device ID/LUN specifier '%s'", l.c_str())
return false;
}
}
else if (!GetAsUnsignedInt(l, id)) {
LOGWARN("Invalid device ID specifier '%s'", l.c_str())
return false;
}
}
if (const auto& it = log_level_mapping.find(level); it != log_level_mapping.end()) {
set_level(it->second);
}
else {
LOGWARN("Invalid log level '%s'", log_level.c_str())
return false;
}
DeviceLogger::SetLogIdAndLun(id, lun);
if (id != -1) {
if (lun == -1) {
LOGINFO("Set log level for device ID %d to '%s'", id, level.c_str())
}
else {
LOGINFO("Set log level for device ID %d, LUN %d to '%s'", id, lun, level.c_str())
}
}
else {
LOGINFO("Set log level to '%s'", level.c_str())
}
return true;
}
bool PiscsiExecutor::Start(PrimaryDevice& device, bool dryRun) const
{
if (!dryRun) {
LOGINFO("Start requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
if (!device.Start()) {
LOGWARN("Starting %s ID %d, unit %d failed", device.GetTypeString(), device.GetId(), device.GetLun())
}
}
return true;
}
bool PiscsiExecutor::Stop(PrimaryDevice& device, bool dryRun) const
{
if (!dryRun) {
LOGINFO("Stop requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
device.Stop();
}
return true;
}
bool PiscsiExecutor::Eject(PrimaryDevice& device, bool dryRun) const
{
if (!dryRun) {
LOGINFO("Eject requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
if (!device.Eject(true)) {
LOGWARN("Ejecting %s ID %d, unit %d failed", device.GetTypeString(), device.GetId(), device.GetLun())
}
}
return true;
}
bool PiscsiExecutor::Protect(PrimaryDevice& device, bool dryRun) const
{
if (!dryRun) {
LOGINFO("Write protection requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(),
device.GetLun())
device.SetProtected(true);
}
return true;
}
bool PiscsiExecutor::Unprotect(PrimaryDevice& device, bool dryRun) const
{
if (!dryRun) {
LOGINFO("Write unprotection requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(),
device.GetLun())
device.SetProtected(false);
}
return true;
}
bool PiscsiExecutor::Attach(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun)
{
const int id = pb_device.id();
const int lun = pb_device.unit();
const PbDeviceType type = pb_device.type();
if (lun >= ScsiController::LUN_MAX) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), to_string(ScsiController::LUN_MAX));
}
if (controller_manager.GetDeviceByIdAndLun(id, lun) != nullptr) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_DUPLICATE_ID, to_string(id), to_string(lun));
}
if (reserved_ids.find(id) != reserved_ids.end()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_RESERVED_ID, to_string(id));
}
const string filename = GetParam(pb_device, "file");
auto device = CreateDevice(context, type, lun, filename);
if (device == nullptr) {
return false;
}
// If no filename was provided the medium is considered not inserted
auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
device->SetRemoved(storage_device != nullptr ? filename.empty() : false);
if (!SetProductData(context, pb_device, *device)) {
return false;
}
if (!SetSectorSize(context, device, pb_device.block_size())) {
return false;
}
string full_path;
if (device->SupportsFile()) {
// Only with removable media drives, CD and MO the medium (=file) may be inserted later
if (!device->IsRemovable() && filename.empty()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME, PbDeviceType_Name(type));
}
if (!ValidateImageFile(context, *storage_device, filename, full_path)) {
return false;
}
}
// Only non read-only devices support protect/unprotect
// This operation must not be executed before Open() because Open() overrides some settings.
if (device->IsProtectable() && !device->IsReadOnly()) {
device->SetProtected(pb_device.protected_());
}
// Stop the dry run here, before actually attaching
if (dryRun) {
return true;
}
unordered_map<string, string> params = { pb_device.params().begin(), pb_device.params().end() };
if (!device->SupportsFile()) {
// Clients like scsictl might have sent both "file" and "interfaces"
params.erase("file");
}
if (!device->Init(params)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_INITIALIZATION, PbDeviceType_Name(device->GetType()),
to_string(id), to_string(lun));
}
if (storage_device != nullptr) {
storage_device->ReserveFile(full_path, id, lun);
}
if (!controller_manager.AttachToScsiController(id, device)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_SCSI_CONTROLLER);
}
string msg = "Attached ";
if (device->IsReadOnly()) {
msg += "read-only ";
}
else if (device->IsProtectable() && device->IsProtected()) {
msg += "protected ";
}
msg += string(device->GetTypeString()) + " device, ID " + to_string(id) + ", unit " + to_string(lun);
LOGINFO("%s", msg.c_str())
return true;
}
bool PiscsiExecutor::Insert(const CommandContext& context, const PbDeviceDefinition& pb_device,
const shared_ptr<PrimaryDevice>& device, bool dryRun) const
{
auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
if (storage_device == nullptr) {
return false;
}
if (!storage_device->IsRemoved()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_EJECT_REQUIRED);
}
if (!pb_device.vendor().empty() || !pb_device.product().empty() || !pb_device.revision().empty()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_DEVICE_NAME_UPDATE);
}
const string filename = GetParam(pb_device, "file");
if (filename.empty()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME);
}
// Stop the dry run here, before modifying the device
if (dryRun) {
return true;
}
LOGINFO("Insert %sfile '%s' requested into %s ID %d, unit %d", pb_device.protected_() ? "protected " : "",
filename.c_str(), storage_device->GetTypeString(), pb_device.id(), pb_device.unit())
if (!SetSectorSize(context, storage_device, pb_device.block_size())) {
return false;
}
string full_path;
if (!ValidateImageFile(context, *storage_device, filename, full_path)) {
return false;
}
storage_device->SetProtected(pb_device.protected_());
storage_device->ReserveFile(full_path, storage_device->GetId(), storage_device->GetLun());
storage_device->SetMediumChanged(true);
return true;
}
bool PiscsiExecutor::Detach(const CommandContext& context, const shared_ptr<PrimaryDevice>& device, bool dryRun) const
{
auto controller = controller_manager.FindController(device->GetId());
if (controller == nullptr) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
}
// LUN 0 can only be detached if there is no other LUN anymore
if (!device->GetLun() && controller->GetLunCount() > 1) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_LUN0);
}
if (!dryRun) {
// Remember the ID before it gets invalid when removing the device
const int id = device->GetId();
if (!controller->RemoveDevice(device)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
}
// If no LUN is left also delete the controller
if (!controller->GetLunCount() && !controller_manager.DeleteController(controller)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
}
if (auto storage_device = dynamic_pointer_cast<StorageDevice>(device); storage_device != nullptr) {
storage_device->UnreserveFile();
}
LOGINFO("%s", ("Detached " + string(device->GetTypeString()) + " device with ID " + to_string(id)
+ ", unit " + to_string(device->GetLun())).c_str())
}
return true;
}
void PiscsiExecutor::DetachAll()
{
controller_manager.DeleteAllControllers();
StorageDevice::UnreserveAll();
LOGINFO("Detached all devices")
}
bool PiscsiExecutor::ShutDown(const CommandContext& context, const string& mode) {
if (mode.empty()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING);
}
PbResult result;
result.set_status(true);
// The PiSCSI shutdown mode is "rascsi" instead of "piscsi" for backwards compatibility
if (mode == "rascsi") {
LOGINFO("PiSCSI shutdown requested")
serializer.SerializeMessage(context.GetFd(), result);
return true;
}
if (mode != "system" && mode != "reboot") {
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, mode);
}
// The root user has UID 0
if (getuid()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_PERMISSION);
}
if (mode == "system") {
LOGINFO("System shutdown requested")
serializer.SerializeMessage(context.GetFd(), result);
DetachAll();
if (system("init 0") == -1) {
LOGERROR("System shutdown failed: %s", strerror(errno))
}
}
else if (mode == "reboot") {
LOGINFO("System reboot requested")
serializer.SerializeMessage(context.GetFd(), result);
DetachAll();
if (system("init 6") == -1) {
LOGERROR("System reboot failed: %s", strerror(errno))
}
}
else {
assert(false);
}
return false;
}
string PiscsiExecutor::SetReservedIds(string_view ids)
{
list<string> ids_to_reserve;
stringstream ss(ids.data());
string id;
while (getline(ss, id, ',')) {
if (!id.empty()) {
ids_to_reserve.push_back(id);
}
}
set<int> reserved;
for (const string& id_to_reserve : ids_to_reserve) {
int res_id;
if (!GetAsUnsignedInt(id_to_reserve, res_id) || res_id > 7) {
return "Invalid ID " + id_to_reserve;
}
if (controller_manager.FindController(res_id) != nullptr) {
return "ID " + id_to_reserve + " is currently in use";
}
reserved.insert(res_id);
}
reserved_ids = { reserved.begin(), reserved.end() };
if (!reserved_ids.empty()) {
string s;
bool isFirst = true;
for (const auto& reserved_id : reserved) {
if (!isFirst) {
s += ", ";
}
isFirst = false;
s += to_string(reserved_id);
}
LOGINFO("Reserved ID(s) set to %s", s.c_str())
}
else {
LOGINFO("Cleared reserved ID(s)")
}
return "";
}
bool PiscsiExecutor::ValidateImageFile(const CommandContext& context, StorageDevice& storage_device,
const string& filename, string& full_path) const
{
if (filename.empty()) {
return true;
}
if (const auto [id1, lun1] = StorageDevice::GetIdsForReservedFile(filename); id1 != -1 || lun1 != -1) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename,
to_string(id1), to_string(lun1));
}
string effective_filename = filename;
if (!StorageDevice::FileExists(filename)) {
// If the file does not exist search for it in the default image folder
effective_filename = piscsi_image.GetDefaultFolder() + "/" + filename;
if (const auto [id2, lun2] = StorageDevice::GetIdsForReservedFile(effective_filename); id2 != -1 || lun2 != -1) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename,
to_string(id2), to_string(lun2));
}
if (!StorageDevice::FileExists(effective_filename)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_FILE_OPEN, effective_filename);
}
}
storage_device.SetFilename(effective_filename);
if (storage_device.IsReadOnlyFile()) {
// Permanently write-protected
storage_device.SetReadOnly(true);
storage_device.SetProtectable(false);
}
else {
storage_device.SetReadOnly(false);
storage_device.SetProtectable(true);
}
storage_device.Open();
full_path = effective_filename;
return true;
}
void PiscsiExecutor::PrintCommand(const PbCommand& command, const PbDeviceDefinition& pb_device, bool dryRun) const
{
const map<string, string, less<>> params = { command.params().begin(), command.params().end() };
ostringstream s;
s << (dryRun ? "Validating" : "Executing");
s << ": operation=" << PbOperation_Name(command.operation());
if (!params.empty()) {
s << ", command params=";
bool isFirst = true;
for (const auto& [key, value]: params) {
if (!isFirst) {
s << ", ";
}
isFirst = false;
string v = key != "token" ? value : "???";
s << "'" << key << "=" << v << "'";
}
}
s << ", device id=" << pb_device.id() << ", lun=" << pb_device.unit() << ", type="
<< PbDeviceType_Name(pb_device.type());
if (pb_device.params_size()) {
s << ", device params=";
bool isFirst = true;
for (const auto& [key, value]: pb_device.params()) {
if (!isFirst) {
s << ":";
}
isFirst = false;
s << "'" << key << "=" << value << "'";
}
}
s << ", vendor='" << pb_device.vendor() << "', product='" << pb_device.product()
<< "', revision='" << pb_device.revision() << "', block size=" << pb_device.block_size();
LOGINFO("%s", s.str().c_str())
}
string PiscsiExecutor::ValidateLunSetup(const PbCommand& command) const
{
// Mapping of available LUNs (bit vector) to devices
unordered_map<uint32_t, uint32_t> luns;
// Collect LUN bit vectors of new devices
for (const auto& device : command.devices()) {
luns[device.id()] |= 1 << device.unit();
}
// Collect LUN bit vectors of existing devices
for (const auto& device : controller_manager.GetAllDevices()) {
luns[device->GetId()] |= 1 << device->GetLun();
}
// LUN 0 must exist for all devices
for (const auto& [id, lun]: luns) {
if (!(lun & 0x01)) {
return "LUN 0 is missing for device ID " + to_string(id);
}
}
return "";
}
bool PiscsiExecutor::VerifyExistingIdAndLun(const CommandContext& context, int id, int lun) const
{
if (controller_manager.FindController(id) == nullptr) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_DEVICE, to_string(id));
}
if (controller_manager.GetDeviceByIdAndLun(id, lun) == nullptr) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_UNIT, to_string(id), to_string(lun));
}
return true;
}
shared_ptr<PrimaryDevice> PiscsiExecutor::CreateDevice(const CommandContext& context, const PbDeviceType type,
int lun, const string& filename) const
{
auto device = device_factory.CreateDevice(type, lun, filename);
if (device == nullptr) {
if (type == UNDEFINED) {
context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, filename);
}
else {
context.ReturnLocalizedError(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, PbDeviceType_Name(type));
}
}
return device;
}
bool PiscsiExecutor::SetSectorSize(const CommandContext& context, shared_ptr<PrimaryDevice> device, int block_size) const
{
if (block_size) {
auto disk = dynamic_pointer_cast<Disk>(device);
if (disk != nullptr && disk->IsSectorSizeConfigurable()) {
if (!disk->SetConfiguredSectorSize(device_factory, block_size)) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE, to_string(block_size));
}
}
else {
return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE,
device->GetTypeString());
}
}
return true;
}
bool PiscsiExecutor::ValidateOperationAgainstDevice(const CommandContext& context, const PrimaryDevice& device,
const PbOperation& operation)
{
if ((operation == START || operation == STOP) && !device.IsStoppable()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, device.GetTypeString());
}
if ((operation == INSERT || operation == EJECT) && !device.IsRemovable()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, device.GetTypeString());
}
if ((operation == PROTECT || operation == UNPROTECT) && !device.IsProtectable()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, device.GetTypeString());
}
if ((operation == PROTECT || operation == UNPROTECT) && !device.IsReady()) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_READY, device.GetTypeString());
}
return true;
}
bool PiscsiExecutor::ValidateIdAndLun(const CommandContext& context, int id, int lun)
{
// Validate the device ID and LUN
if (id < 0) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_ID);
}
if (id >= ControllerManager::DEVICE_MAX) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_ID, to_string(id), to_string(ControllerManager::DEVICE_MAX - 1));
}
if (lun < 0 || lun >= ScsiController::LUN_MAX) {
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), to_string(ScsiController::LUN_MAX - 1));
}
return true;
}
bool PiscsiExecutor::SetProductData(const CommandContext& context, const PbDeviceDefinition& pb_device,
PrimaryDevice& device)
{
try {
if (!pb_device.vendor().empty()) {
device.SetVendor(pb_device.vendor());
}
if (!pb_device.product().empty()) {
device.SetProduct(pb_device.product());
}
if (!pb_device.revision().empty()) {
device.SetRevision(pb_device.revision());
}
}
catch(const invalid_argument& e) {
return context.ReturnStatus(false, e.what());
}
return true;
}
+83
View File
@@ -0,0 +1,83 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "spdlog/spdlog.h"
#include "shared/protobuf_serializer.h"
#include "piscsi/piscsi_response.h"
#include <unordered_set>
#include <unordered_map>
class PiscsiImage;
class DeviceFactory;
class ControllerManager;
class PrimaryDevice;
class CommandContext;
using namespace spdlog;
class PiscsiExecutor
{
public:
PiscsiExecutor(PiscsiImage& piscsi_image, ControllerManager& controller_manager)
: piscsi_image(piscsi_image), controller_manager(controller_manager) {}
~PiscsiExecutor() = default;
unordered_set<int> GetReservedIds() const { return reserved_ids; }
bool ProcessDeviceCmd(const CommandContext&, const PbDeviceDefinition&, const PbCommand&, bool);
bool ProcessCmd(const CommandContext&, const PbCommand&);
bool SetLogLevel(const string&) const;
bool Start(PrimaryDevice&, bool) const;
bool Stop(PrimaryDevice&, bool) const;
bool Eject(PrimaryDevice&, bool) const;
bool Protect(PrimaryDevice&, bool) const;
bool Unprotect(PrimaryDevice&, bool) const;
bool Attach(const CommandContext&, const PbDeviceDefinition&, bool);
bool Insert(const CommandContext&, const PbDeviceDefinition&, const shared_ptr<PrimaryDevice>&, bool) const;
bool Detach(const CommandContext&, const shared_ptr<PrimaryDevice>&, bool) const;
void DetachAll();
bool ShutDown(const CommandContext&, const string&);
string SetReservedIds(string_view);
bool ValidateImageFile(const CommandContext&, StorageDevice&, const string&, string&) const;
void PrintCommand(const PbCommand&, const PbDeviceDefinition&, bool) const;
string ValidateLunSetup(const PbCommand&) const;
bool VerifyExistingIdAndLun(const CommandContext&, int, int) const;
shared_ptr<PrimaryDevice> CreateDevice(const CommandContext&, const PbDeviceType, int, const string&) const;
bool SetSectorSize(const CommandContext&, shared_ptr<PrimaryDevice>, int) const;
static bool ValidateOperationAgainstDevice(const CommandContext&, const PrimaryDevice&, const PbOperation&);
static bool ValidateIdAndLun(const CommandContext&, int, int);
static bool SetProductData(const CommandContext&, const PbDeviceDefinition&, PrimaryDevice&);
private:
const PiscsiResponse piscsi_response;
PiscsiImage& piscsi_image;
ControllerManager& controller_manager;
const DeviceFactory device_factory;
const ProtobufSerializer serializer;
unordered_set<int> reserved_ids;
static inline const unordered_map<string, level::level_enum> log_level_mapping = {
{ "trace", level::trace },
{ "debug", level::debug },
{ "info", level::info },
{ "warn", level::warn },
{ "err", level::err },
{ "off", level::off }
};
};
+441
View File
@@ -0,0 +1,441 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "shared/log.h"
#include "shared/protobuf_util.h"
#include "devices/disk.h"
#include "command_context.h"
#include "piscsi_image.h"
#include <unistd.h>
#include <pwd.h>
#include <fstream>
#include <string>
#include <array>
#include <filesystem>
using namespace std;
using namespace filesystem;
using namespace piscsi_interface;
using namespace protobuf_util;
PiscsiImage::PiscsiImage()
{
// ~/images is the default folder for device image files, for the root user it is /home/pi/images
default_folder = GetHomeDir() + "/images";
}
bool PiscsiImage::CheckDepth(string_view filename) const
{
return count(filename.begin(), filename.end(), '/') <= depth;
}
bool PiscsiImage::CreateImageFolder(const CommandContext& context, const string& filename) const
{
if (const size_t filename_start = filename.rfind('/'); filename_start != string::npos) {
const auto folder = path(filename.substr(0, filename_start));
// Checking for existence first prevents an error if the top-level folder is a softlink
if (error_code error; exists(folder, error)) {
return true;
}
try {
create_directories(folder);
return ChangeOwner(context, folder, false);
}
catch(const filesystem_error& e) {
return context.ReturnStatus(false, "Can't create image folder '" + string(folder) + "': " + e.what());
}
}
return true;
}
string PiscsiImage::SetDefaultFolder(const string& f)
{
if (f.empty()) {
return "Can't set default image folder: Missing folder name";
}
string folder = f;
// If a relative path is specified, the path is assumed to be relative to the user's home directory
if (folder[0] != '/') {
folder = GetHomeDir() + "/" + folder;
}
else {
if (folder.find("/home/") != 0) {
return "Default image folder must be located in '/home/'";
}
}
// Resolve a potential symlink
auto p = path(folder);
if (error_code error; is_symlink(p, error)) {
p = read_symlink(p);
}
if (error_code error; !is_directory(p, error)) {
return "'" + string(p) + "' is not a valid folder";
}
default_folder = string(p);
LOGINFO("Default image folder set to '%s'", default_folder.c_str())
return "";
}
bool PiscsiImage::CreateImage(const CommandContext& context, const PbCommand& command) const
{
const string filename = GetParam(command, "file");
if (filename.empty()) {
return context.ReturnStatus(false, "Can't create image file: Missing image filename");
}
if (!CheckDepth(filename)) {
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
const string full_filename = GetFullName(filename);
if (!IsValidDstFilename(full_filename)) {
return context.ReturnStatus(false, "Can't create image file: '" + full_filename + "': File already exists");
}
const string size = GetParam(command, "size");
if (size.empty()) {
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Missing file size");
}
off_t len;
try {
len = stoull(size);
}
catch(const invalid_argument&) {
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
}
catch(const out_of_range&) {
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
}
if (len < 512 || (len & 0x1ff)) {
return context.ReturnStatus(false, "Invalid image file size " + to_string(len) + " (not a multiple of 512)");
}
if (!CreateImageFolder(context, full_filename)) {
return false;
}
const bool read_only = GetParam(command, "read_only") == "true";
error_code error;
path file(full_filename);
try {
ofstream s(file);
s.close();
if (!ChangeOwner(context, file, read_only)) {
return false;
}
resize_file(file, len);
}
catch(const filesystem_error& e) {
remove(file, error);
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': " + e.what());
}
LOGINFO("%s", string("Created " + string(read_only ? "read-only " : "") + "image file '" + full_filename +
"' with a size of " + to_string(len) + " bytes").c_str())
return context.ReturnStatus();
}
bool PiscsiImage::DeleteImage(const CommandContext& context, const PbCommand& command) const
{
const string filename = GetParam(command, "file");
if (filename.empty()) {
return context.ReturnStatus(false, "Missing image filename");
}
if (!CheckDepth(filename)) {
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
const auto full_filename = path(GetFullName(filename));
if (!exists(full_filename)) {
return context.ReturnStatus(false, "Image file '" + string(full_filename) + "' does not exist");
}
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(full_filename);
if (id != -1 || lun != -1) {
return context.ReturnStatus(false, "Can't delete image file '" + string(full_filename) +
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
}
if (error_code error; !remove(full_filename, error)) {
return context.ReturnStatus(false, "Can't delete image file '" + string(full_filename) + "'");
}
// Delete empty subfolders
size_t last_slash = filename.rfind('/');
while (last_slash != string::npos) {
const string folder = filename.substr(0, last_slash);
const auto full_folder = path(GetFullName(folder));
if (error_code error; !filesystem::is_empty(full_folder, error) || error) {
break;
}
if (error_code error; !remove(full_folder)) {
return context.ReturnStatus(false, "Can't delete empty image folder '" + string(full_folder) + "'");
}
last_slash = folder.rfind('/');
}
LOGINFO("Deleted image file '%s'", full_filename.c_str())
return context.ReturnStatus();
}
bool PiscsiImage::RenameImage(const CommandContext& context, const PbCommand& command) const
{
string from;
string to;
if (!ValidateParams(context, command, "rename/move", from, to)) {
return false;
}
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(from);
if (id != -1 || lun != -1) {
return context.ReturnStatus(false, "Can't rename/move image file '" + from +
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
}
if (!CreateImageFolder(context, to)) {
return false;
}
try {
rename(path(from), path(to));
}
catch(const filesystem_error& e) {
return context.ReturnStatus(false, "Can't rename/move image file '" + from + "' to '" + to + "': " + e.what());
}
LOGINFO("Renamed/Moved image file '%s' to '%s'", from.c_str(), to.c_str())
return context.ReturnStatus();
}
bool PiscsiImage::CopyImage(const CommandContext& context, const PbCommand& command) const
{
string from;
string to;
if (!ValidateParams(context, command, "copy", from, to)) {
return false;
}
if (access(from.c_str(), R_OK)) {
return context.ReturnStatus(false, "Can't read source image file '" + from + "'");
}
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(from);
if (id != -1 || lun != -1) {
return context.ReturnStatus(false, "Can't copy image file '" + from +
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
}
if (!CreateImageFolder(context, to)) {
return false;
}
path f(from);
path t(to);
// Symbolic links need a special handling
if (error_code error; is_symlink(f, error)) {
try {
copy_symlink(f, t);
}
catch(const filesystem_error& e) {
return context.ReturnStatus(false, "Can't copy image file symlink '" + from + "': " + e.what());
}
LOGINFO("Copied image file symlink '%s' to '%s'", from.c_str(), to.c_str())
return context.ReturnStatus();
}
try {
copy_file(f, t);
permissions(t, GetParam(command, "read_only") == "true" ?
perms::owner_read | perms::group_read | perms::others_read :
perms::owner_read | perms::group_read | perms::others_read |
perms::owner_write | perms::group_write);
}
catch(const filesystem_error& e) {
return context.ReturnStatus(false, "Can't copy image file '" + from + "' to '" + to + "': " + e.what());
}
LOGINFO("Copied image file '%s' to '%s'", from.c_str(), to.c_str())
return context.ReturnStatus();
}
bool PiscsiImage::SetImagePermissions(const CommandContext& context, const PbCommand& command) const
{
string filename = GetParam(command, "file");
if (filename.empty()) {
return context.ReturnStatus(false, "Missing image filename");
}
if (!CheckDepth(filename)) {
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
filename = GetFullName(filename);
if (!IsValidSrcFilename(filename)) {
return context.ReturnStatus(false, "Can't modify image file '" + filename + "': Invalid name or type");
}
const bool protect = command.operation() == PROTECT_IMAGE;
try {
permissions(path(filename), protect ?
perms::owner_read | perms::group_read | perms::others_read :
perms::owner_read | perms::group_read | perms::others_read |
perms::owner_write | perms::group_write);
}
catch(const filesystem_error& e) {
return context.ReturnStatus(false, "Can't " + string(protect ? "protect" : "unprotect") + " image file '" +
filename + "': " + e.what());
}
if (protect) {
LOGINFO("Protected image file '%s'", filename.c_str())
}
else {
LOGINFO("Unprotected image file '%s'", filename.c_str())
}
return context.ReturnStatus();
}
bool PiscsiImage::ValidateParams(const CommandContext& context, const PbCommand& command, const string& operation,
string& from, string& to) const
{
from = GetParam(command, "from");
if (from.empty()) {
return context.ReturnStatus(false, "Can't " + operation + " image file: Missing source filename");
}
if (!CheckDepth(from)) {
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + from + "'").c_str());
}
from = GetFullName(from);
if (!IsValidSrcFilename(from)) {
return context.ReturnStatus(false, "Can't " + operation + " image file: '" + from + "': Invalid name or type");
}
to = GetParam(command, "to");
if (to.empty()) {
return context.ReturnStatus(false, "Can't " + operation + " image file '" + from + "': Missing destination filename");
}
if (!CheckDepth(to)) {
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + to + "'").c_str());
}
to = GetFullName(to);
if (!IsValidDstFilename(to)) {
return context.ReturnStatus(false, "Can't " + operation + " image file '" + from + "' to '" + to + "': File already exists");
}
return true;
}
bool PiscsiImage::IsValidSrcFilename(const string& filename)
{
// Source file must exist and must be a regular file or a symlink
path file(filename);
return is_regular_file(file) || is_symlink(file);
}
bool PiscsiImage::IsValidDstFilename(const string& filename)
{
// Destination file must not yet exist
try {
return !exists(path(filename));
}
catch(const filesystem_error&) {
return true;
}
}
bool PiscsiImage::ChangeOwner(const CommandContext& context, const path& filename, bool read_only)
{
const auto [uid, gid] = GetUidAndGid();
if (chown(filename.c_str(), uid, gid)) {
// Remember the current error before the next filesystem operation
const int e = errno;
error_code error;
remove(filename, error);
return context.ReturnStatus(false, "Can't change ownership of '" + string(filename) + "': " + strerror(e));
}
permissions(filename, read_only ?
perms::owner_read | perms::group_read | perms::others_read :
perms::owner_read | perms::group_read | perms::others_read |
perms::owner_write | perms::group_write);
return true;
}
string PiscsiImage::GetHomeDir()
{
const auto [uid, gid] = GetUidAndGid();
passwd pwd = {};
passwd *p_pwd;
array<char, 256> pwbuf;
if (uid && !getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) {
return pwd.pw_dir;
}
else {
return "/home/pi";
}
}
pair<int, int> PiscsiImage::GetUidAndGid()
{
int uid = getuid();
if (const char *sudo_user = getenv("SUDO_UID"); sudo_user != nullptr) {
uid = stoi(sudo_user);
}
passwd pwd = {};
passwd *p_pwd;
array<char, 256> pwbuf;
int gid = -1;
if (!getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) {
gid = pwd.pw_gid;
}
return make_pair(uid, gid);
}
+54
View File
@@ -0,0 +1,54 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "generated/piscsi_interface.pb.h"
#include "command_context.h"
#include <string>
#include <filesystem>
using namespace std;
using namespace filesystem;
using namespace piscsi_interface;
class PiscsiImage
{
public:
PiscsiImage();
~PiscsiImage() = default;
void SetDepth(int d) { depth = d; }
int GetDepth() const { return depth; }
string GetDefaultFolder() const { return default_folder; }
string SetDefaultFolder(const string&);
bool CreateImage(const CommandContext&, const PbCommand&) const;
bool DeleteImage(const CommandContext&, const PbCommand&) const;
bool RenameImage(const CommandContext&, const PbCommand&) const;
bool CopyImage(const CommandContext&, const PbCommand&) const;
bool SetImagePermissions(const CommandContext&, const PbCommand&) const;
private:
bool CheckDepth(string_view) const;
string GetFullName(const string& filename) const { return default_folder + "/" + filename; }
bool CreateImageFolder(const CommandContext&, const string&) const;
bool ValidateParams(const CommandContext&, const PbCommand&, const string&, string&, string&) const;
static bool IsValidSrcFilename(const string&);
static bool IsValidDstFilename(const string&);
static bool ChangeOwner(const CommandContext&, const path&, bool);
static string GetHomeDir();
static pair<int, int> GetUidAndGid();
string default_folder;
int depth = 1;
};
+551
View File
@@ -0,0 +1,551 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "shared/log.h"
#include "shared/protobuf_util.h"
#include "shared/piscsi_version.h"
#include "devices/disk.h"
#include "devices/device_factory.h"
#include "generated/piscsi_interface.pb.h"
#include "piscsi_response.h"
#include <filesystem>
using namespace std;
using namespace filesystem;
using namespace piscsi_interface;
using namespace protobuf_util;
unique_ptr<PbDeviceProperties> PiscsiResponse::GetDeviceProperties(const Device& device) const
{
auto properties = make_unique<PbDeviceProperties>();
// Currently there is only a SCSI controller, i.e. there can always be 32 LUNs
properties->set_luns(32);
properties->set_read_only(device.IsReadOnly());
properties->set_protectable(device.IsProtectable());
properties->set_stoppable(device.IsStoppable());
properties->set_removable(device.IsRemovable());
properties->set_lockable(device.IsLockable());
properties->set_supports_file(device.SupportsFile());
properties->set_supports_params(device.SupportsParams());
if (device.SupportsParams()) {
for (const auto& [key, value] : device_factory.GetDefaultParams(device.GetType())) {
auto& map = *properties->mutable_default_params();
map[key] = value;
}
}
for (const auto& block_size : device_factory.GetSectorSizes(device.GetType())) {
properties->add_block_sizes(block_size);
}
return properties;
}
void PiscsiResponse::GetDeviceTypeProperties(PbDeviceTypesInfo& device_types_info, PbDeviceType type) const
{
auto type_properties = device_types_info.add_properties();
type_properties->set_type(type);
const auto device = device_factory.CreateDevice(type, 0, "");
type_properties->set_allocated_properties(GetDeviceProperties(*device).release());
} //NOSONAR The allocated memory is managed by protobuf
void PiscsiResponse::GetAllDeviceTypeProperties(PbDeviceTypesInfo& device_types_info) const
{
// Start with 2 instead of 1. 1 was the removed SASI drive type.
int ordinal = 2;
while (PbDeviceType_IsValid(ordinal)) {
PbDeviceType type = UNDEFINED;
PbDeviceType_Parse(PbDeviceType_Name((PbDeviceType)ordinal), &type);
GetDeviceTypeProperties(device_types_info, type);
ordinal++;
}
}
void PiscsiResponse::GetDevice(const Device& device, PbDevice& pb_device, const string& default_folder) const
{
pb_device.set_id(device.GetId());
pb_device.set_unit(device.GetLun());
pb_device.set_vendor(device.GetVendor());
pb_device.set_product(device.GetProduct());
pb_device.set_revision(device.GetRevision());
pb_device.set_type(device.GetType());
pb_device.set_allocated_properties(GetDeviceProperties(device).release());
auto status = make_unique<PbDeviceStatus>().release(); //NOSONAR The allocated memory is managed by protobuf
pb_device.set_allocated_status(status);
status->set_protected_(device.IsProtected());
status->set_stopped(device.IsStopped());
status->set_removed(device.IsRemoved());
status->set_locked(device.IsLocked());
if (device.SupportsParams()) { //NOSONAR The allocated memory is managed by protobuf
for (const auto& [key, value] : device.GetParams()) {
SetParam(pb_device, key, value);
}
}
if (const auto disk = dynamic_cast<const Disk*>(&device); disk) {
pb_device.set_block_size(device.IsRemoved()? 0 : disk->GetSectorSizeInBytes());
pb_device.set_block_count(device.IsRemoved() ? 0: disk->GetBlockCount());
}
const auto storage_device = dynamic_cast<const StorageDevice *>(&device);
if (storage_device != nullptr) {
auto image_file = make_unique<PbImageFile>().release();
GetImageFile(*image_file, default_folder, device.IsReady() ? storage_device->GetFilename() : "");
pb_device.set_allocated_file(image_file);
}
} //NOSONAR The allocated memory is managed by protobuf
bool PiscsiResponse::GetImageFile(PbImageFile& image_file, const string& default_folder, const string& filename) const
{
if (!filename.empty()) {
image_file.set_name(filename);
image_file.set_type(device_factory.GetTypeForFile(filename));
const string f = filename[0] == '/' ? filename : default_folder + "/" + filename;
image_file.set_read_only(access(f.c_str(), W_OK));
// filesystem::file_size cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
if (struct stat st; !stat(f.c_str(), &st) && !S_ISDIR(st.st_mode)) {
image_file.set_size(st.st_size);
return true;
}
}
return false;
}
void PiscsiResponse::GetAvailableImages(PbImageFilesInfo& image_files_info, const string& default_folder,
const string& folder, const string& folder_pattern, const string& file_pattern, int scan_depth) const
{
if (scan_depth-- < 0) {
return;
}
string folder_pattern_lower = folder_pattern;
transform(folder_pattern_lower.begin(), folder_pattern_lower.end(), folder_pattern_lower.begin(), ::tolower);
string file_pattern_lower = file_pattern;
transform(file_pattern_lower.begin(), file_pattern_lower.end(), file_pattern_lower.begin(), ::tolower);
DIR *d = opendir(folder.c_str());
if (d == nullptr) {
return;
}
// C++ filesystem cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
const dirent *dir;
while ((dir = readdir(d))) {
string filename = GetNextImageFile(dir, folder);
if (filename.empty()) {
continue;
}
string name_lower = dir->d_name;
if (!file_pattern.empty()) {
transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower);
}
if (dir->d_type == DT_DIR) {
if (folder_pattern_lower.empty() || name_lower.find(folder_pattern_lower) != string::npos) {
GetAvailableImages(image_files_info, default_folder, filename, folder_pattern,
file_pattern, scan_depth);
}
continue;
}
if (file_pattern_lower.empty() || name_lower.find(file_pattern_lower) != string::npos) {
if (auto image_file = make_unique<PbImageFile>(); GetImageFile(*image_file.get(), default_folder, filename)) {
GetImageFile(*image_files_info.add_image_files(), default_folder,
filename.substr(default_folder.length() + 1));
}
}
}
closedir(d);
}
unique_ptr<PbImageFilesInfo> PiscsiResponse::GetAvailableImages(PbResult& result, const string& default_folder,
const string& folder_pattern, const string& file_pattern, int scan_depth) const
{
auto image_files_info = make_unique<PbImageFilesInfo>();
image_files_info->set_default_image_folder(default_folder);
image_files_info->set_depth(scan_depth);
GetAvailableImages(*image_files_info, default_folder, default_folder, folder_pattern,
file_pattern, scan_depth);
result.set_status(true);
return image_files_info;
}
void PiscsiResponse::GetAvailableImages(PbResult& result, PbServerInfo& server_info, const string& default_folder,
const string& folder_pattern, const string& file_pattern, int scan_depth) const
{
auto image_files_info = GetAvailableImages(result, default_folder, folder_pattern, file_pattern, scan_depth);
image_files_info->set_default_image_folder(default_folder);
server_info.set_allocated_image_files_info(image_files_info.release());
result.set_status(true); //NOSONAR The allocated memory is managed by protobuf
}
unique_ptr<PbReservedIdsInfo> PiscsiResponse::GetReservedIds(PbResult& result, const unordered_set<int>& ids) const
{
auto reserved_ids_info = make_unique<PbReservedIdsInfo>();
for (const int id : ids) {
reserved_ids_info->add_ids(id);
}
result.set_status(true);
return reserved_ids_info;
}
void PiscsiResponse::GetDevices(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbServerInfo& server_info,
const string& default_folder) const
{
for (const auto& device : devices) {
PbDevice *pb_device = server_info.mutable_devices_info()->add_devices();
GetDevice(*device, *pb_device, default_folder);
}
}
void PiscsiResponse::GetDevicesInfo(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbResult& result,
const PbCommand& command, const string& default_folder) const
{
set<id_set> id_sets;
// If no device list was provided in the command get information on all devices
if (!command.devices_size()) {
for (const auto& device : devices) {
id_sets.insert(make_pair(device->GetId(), device->GetLun()));
}
}
// Otherwise get information on the devices provided in the command
else {
id_sets = MatchDevices(devices, result, command);
if (id_sets.empty()) {
return;
}
}
auto devices_info = make_unique<PbDevicesInfo>();
for (const auto& [id, lun] : id_sets) {
for (const auto& d : devices) {
if (d->GetId() == id && d->GetLun() == lun) {
GetDevice(*d, *devices_info->add_devices(), default_folder);
break;
}
}
}
result.set_allocated_devices_info(devices_info.release());
result.set_status(true);
}
unique_ptr<PbDeviceTypesInfo> PiscsiResponse::GetDeviceTypesInfo(PbResult& result) const
{
auto device_types_info = make_unique<PbDeviceTypesInfo>();
GetAllDeviceTypeProperties(*device_types_info);
result.set_status(true);
return device_types_info;
}
unique_ptr<PbServerInfo> PiscsiResponse::GetServerInfo(const unordered_set<shared_ptr<PrimaryDevice>>& devices,
PbResult& result, const unordered_set<int>& reserved_ids, const string& current_log_level,
const string& default_folder, const string& folder_pattern, const string& file_pattern, int scan_depth) const
{
auto server_info = make_unique<PbServerInfo>();
server_info->set_allocated_version_info(GetVersionInfo(result).release());
server_info->set_allocated_log_level_info(GetLogLevelInfo(result, current_log_level).release()); //NOSONAR The allocated memory is managed by protobuf
GetAllDeviceTypeProperties(*server_info->mutable_device_types_info()); //NOSONAR The allocated memory is managed by protobuf
GetAvailableImages(result, *server_info, default_folder, folder_pattern, file_pattern, scan_depth);
server_info->set_allocated_network_interfaces_info(GetNetworkInterfacesInfo(result).release());
server_info->set_allocated_mapping_info(GetMappingInfo(result).release()); //NOSONAR The allocated memory is managed by protobuf
GetDevices(devices, *server_info, default_folder); //NOSONAR The allocated memory is managed by protobuf
server_info->set_allocated_reserved_ids_info(GetReservedIds(result, reserved_ids).release());
server_info->set_allocated_operation_info(GetOperationInfo(result, scan_depth).release()); //NOSONAR The allocated memory is managed by protobuf
result.set_status(true);
return server_info;
}
unique_ptr<PbVersionInfo> PiscsiResponse::GetVersionInfo(PbResult& result) const
{
auto version_info = make_unique<PbVersionInfo>();
version_info->set_major_version(piscsi_major_version);
version_info->set_minor_version(piscsi_minor_version);
version_info->set_patch_version(piscsi_patch_version);
result.set_status(true);
return version_info;
}
unique_ptr<PbLogLevelInfo> PiscsiResponse::GetLogLevelInfo(PbResult& result, const string& current_log_level) const
{
auto log_level_info = make_unique<PbLogLevelInfo>();
for (const auto& log_level : log_levels) {
log_level_info->add_log_levels(log_level);
}
log_level_info->set_current_log_level(current_log_level);
result.set_status(true);
return log_level_info;
}
unique_ptr<PbNetworkInterfacesInfo> PiscsiResponse::GetNetworkInterfacesInfo(PbResult& result) const
{
auto network_interfaces_info = make_unique<PbNetworkInterfacesInfo>();
for (const auto& network_interface : device_factory.GetNetworkInterfaces()) {
network_interfaces_info->add_name(network_interface);
}
result.set_status(true);
return network_interfaces_info;
}
unique_ptr<PbMappingInfo> PiscsiResponse::GetMappingInfo(PbResult& result) const
{
auto mapping_info = make_unique<PbMappingInfo>();
for (const auto& [name, type] : device_factory.GetExtensionMapping()) {
(*mapping_info->mutable_mapping())[name] = type;
}
result.set_status(true);
return mapping_info;
}
unique_ptr<PbOperationInfo> PiscsiResponse::GetOperationInfo(PbResult& result, int depth) const
{
auto operation_info = make_unique<PbOperationInfo>();
auto operation = CreateOperation(*operation_info, ATTACH, "Attach device, device-specific parameters are required");
AddOperationParameter(*operation, "name", "Image file name in case of a mass storage device").release();
AddOperationParameter(*operation, "interface", "Comma-separated prioritized network interface list").release();
AddOperationParameter(*operation, "inet", "IP address and netmask of the network bridge").release();
AddOperationParameter(*operation, "cmd", "Print command for the printer device").release();
operation.release();
CreateOperation(*operation_info, DETACH, "Detach device, device-specific parameters are required").release();
CreateOperation(*operation_info, DETACH_ALL, "Detach all devices").release();
CreateOperation(*operation_info, START, "Start device, device-specific parameters are required").release();
CreateOperation(*operation_info, STOP, "Stop device, device-specific parameters are required").release();
operation = CreateOperation(*operation_info, INSERT, "Insert medium, device-specific parameters are required");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
operation.release();
CreateOperation(*operation_info, EJECT, "Eject medium, device-specific parameters are required").release();
CreateOperation(*operation_info, PROTECT, "Protect medium, device-specific parameters are required").release();
CreateOperation(*operation_info, UNPROTECT, "Unprotect medium, device-specific parameters are required").release();
operation = CreateOperation(*operation_info, SERVER_INFO, "Get piscsi server information");
if (depth) {
AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names").release();
}
AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names").release();
operation.release();
CreateOperation(*operation_info, VERSION_INFO, "Get piscsi server version").release();
CreateOperation(*operation_info, DEVICES_INFO, "Get information on attached devices").release();
CreateOperation(*operation_info, DEVICE_TYPES_INFO, "Get device properties by device type").release();
operation = CreateOperation(*operation_info, DEFAULT_IMAGE_FILES_INFO, "Get information on available image files");
if (depth) {
AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names").release();
}
AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names").release();
operation.release();
operation = CreateOperation(*operation_info, IMAGE_FILE_INFO, "Get information on image file");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
operation.release();
CreateOperation(*operation_info, LOG_LEVEL_INFO, "Get log level information").release();
CreateOperation(*operation_info, NETWORK_INTERFACES_INFO, "Get the available network interfaces").release();
CreateOperation(*operation_info, MAPPING_INFO, "Get mapping of extensions to device types").release();
CreateOperation(*operation_info, RESERVED_IDS_INFO, "Get list of reserved device IDs").release();
operation = CreateOperation(*operation_info, DEFAULT_FOLDER, "Set default image file folder");
AddOperationParameter(*operation, "folder", "Default image file folder name", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, LOG_LEVEL, "Set log level");
AddOperationParameter(*operation, "level", "New log level", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, RESERVE_IDS, "Reserve device IDs");
AddOperationParameter(*operation, "ids", "Comma-separated device ID list", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, SHUT_DOWN, "Shut down or reboot");
auto parameter = AddOperationParameter(*operation, "mode", "Shutdown mode", "", true).release();
parameter->add_permitted_values("piscsi");
// System shutdown/reboot requires root permissions
if (!getuid()) {
parameter->add_permitted_values("system");
parameter->add_permitted_values("reboot");
}
operation.release();
operation = CreateOperation(*operation_info, CREATE_IMAGE, "Create an image file");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
AddOperationParameter(*operation, "size", "Image file size in bytes", "", true).release();
parameter = AddOperationParameter(*operation, "read_only", "Read-only flag", "false").release();
parameter->add_permitted_values("true");
parameter->add_permitted_values("false");
operation.release();
operation = CreateOperation(*operation_info, DELETE_IMAGE, "Delete image file");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, RENAME_IMAGE, "Rename image file");
AddOperationParameter(*operation, "from", "Source image file name", "", true).release();
AddOperationParameter(*operation, "to", "Destination image file name", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, COPY_IMAGE, "Copy image file");
AddOperationParameter(*operation, "from", "Source image file name", "", true).release();
AddOperationParameter(*operation, "to", "Destination image file name", "", true).release();
parameter = AddOperationParameter(*operation, "read_only", "Read-only flag", "false").release();
parameter->add_permitted_values("true");
parameter->add_permitted_values("false");
operation.release();
operation = CreateOperation(*operation_info, PROTECT_IMAGE, "Write-protect image file");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, UNPROTECT_IMAGE, "Make image file writable");
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
operation.release();
operation = CreateOperation(*operation_info, CHECK_AUTHENTICATION, "Check whether an authentication token is valid");
AddOperationParameter(*operation, "token", "Authentication token to be checked", "", true).release();
operation.release();
CreateOperation(*operation_info, OPERATION_INFO, "Get operation meta data").release();
result.set_status(true);
return operation_info;
}
unique_ptr<PbOperationMetaData> PiscsiResponse::CreateOperation(PbOperationInfo& operation_info, const PbOperation& operation,
const string& description) const
{
auto meta_data = make_unique<PbOperationMetaData>();
meta_data->set_server_side_name(PbOperation_Name(operation));
meta_data->set_description(description);
int ordinal = PbOperation_descriptor()->FindValueByName(PbOperation_Name(operation))->index();
(*operation_info.mutable_operations())[ordinal] = *meta_data.release();
return unique_ptr<PbOperationMetaData>(&(*operation_info.mutable_operations())[ordinal]);
}
unique_ptr<PbOperationParameter> PiscsiResponse::AddOperationParameter(PbOperationMetaData& meta_data,
const string& name, const string& description, const string& default_value, bool is_mandatory) const
{
auto parameter = unique_ptr<PbOperationParameter>(meta_data.add_parameters());
parameter->set_name(name);
parameter->set_description(description);
parameter->set_default_value(default_value);
parameter->set_is_mandatory(is_mandatory);
return parameter;
}
set<id_set> PiscsiResponse::MatchDevices(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbResult& result,
const PbCommand& command) const
{
set<id_set> id_sets;
for (const auto& device : command.devices()) {
bool has_device = false;
for (const auto& d : devices) {
if (d->GetId() == device.id() && d->GetLun() == device.unit()) {
id_sets.insert(make_pair(device.id(), device.unit()));
has_device = true;
break;
}
}
if (!has_device) {
id_sets.clear();
result.set_status(false);
result.set_msg("No device for ID " + to_string(device.id()) + ", unit " + to_string(device.unit()));
break;
}
}
return id_sets;
}
string PiscsiResponse::GetNextImageFile(const dirent *dir, const string& folder)
{
// Ignore unknown folder types and folder names starting with '.'
if ((dir->d_type != DT_REG && dir->d_type != DT_DIR && dir->d_type != DT_LNK && dir->d_type != DT_BLK)
|| dir->d_name[0] == '.') {
return "";
}
const string filename = folder + "/" + dir->d_name;
const bool file_exists = exists(path(filename));
// filesystem::file_size cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
struct stat st;
stat(filename.c_str(), &st);
if (dir->d_type == DT_REG && file_exists && !st.st_size) {
LOGWARN("File '%s' in image folder '%s' is empty", dir->d_name, folder.c_str())
return "";
}
if (dir->d_type == DT_LNK && !file_exists) {
LOGWARN("Symlink '%s' in image folder '%s' is broken", dir->d_name, folder.c_str())
return "";
}
return filename;
}
+63
View File
@@ -0,0 +1,63 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "devices/device_factory.h"
#include "devices/primary_device.h"
#include "generated/piscsi_interface.pb.h"
#include <dirent.h>
#include <array>
#include <string>
using namespace std;
using namespace piscsi_interface;
class PiscsiResponse
{
using id_set = pair<int, int>;
public:
PiscsiResponse() = default;
~PiscsiResponse() = default;
bool GetImageFile(PbImageFile&, const string&, const string&) const;
unique_ptr<PbImageFilesInfo> GetAvailableImages(PbResult&, const string&, const string&, const string&, int) const;
unique_ptr<PbReservedIdsInfo> GetReservedIds(PbResult&, const unordered_set<int>&) const;
void GetDevices(const unordered_set<shared_ptr<PrimaryDevice>>&, PbServerInfo&, const string&) const;
void GetDevicesInfo(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const PbCommand&, const string&) const;
unique_ptr<PbDeviceTypesInfo> GetDeviceTypesInfo(PbResult&) const;
unique_ptr<PbVersionInfo> GetVersionInfo(PbResult&) const;
unique_ptr<PbServerInfo> GetServerInfo(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const unordered_set<int>&,
const string&, const string&, const string&, const string&, int) const;
unique_ptr<PbNetworkInterfacesInfo> GetNetworkInterfacesInfo(PbResult&) const;
unique_ptr<PbMappingInfo> GetMappingInfo(PbResult&) const;
unique_ptr<PbLogLevelInfo> GetLogLevelInfo(PbResult&, const string&) const;
unique_ptr<PbOperationInfo> GetOperationInfo(PbResult&, int) const;
private:
DeviceFactory device_factory;
const inline static array<string, 6> log_levels = { "trace", "debug", "info", "warn", "err", "off" };
unique_ptr<PbDeviceProperties> GetDeviceProperties(const Device&) const;
void GetDevice(const Device&, PbDevice&, const string&) const;
void GetAllDeviceTypeProperties(PbDeviceTypesInfo&) const;
void GetDeviceTypeProperties(PbDeviceTypesInfo&, PbDeviceType) const;
void GetAvailableImages(PbImageFilesInfo&, const string&, const string&, const string&, const string&, int) const;
void GetAvailableImages(PbResult& result, PbServerInfo&, const string&, const string&, const string&, int) const;
unique_ptr<PbOperationMetaData> CreateOperation(PbOperationInfo&, const PbOperation&, const string&) const;
unique_ptr<PbOperationParameter> AddOperationParameter(PbOperationMetaData&, const string&, const string&,
const string& = "", bool = false) const;
set<id_set> MatchDevices(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const PbCommand&) const;
static string GetNextImageFile(const dirent *, const string&);
};
+142
View File
@@ -0,0 +1,142 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#include "shared/log.h"
#include "shared/piscsi_util.h"
#include "shared/protobuf_serializer.h"
#include "shared/piscsi_exceptions.h"
#include "command_context.h"
#include "localizer.h"
#include "piscsi_service.h"
#include <netinet/in.h>
#include <csignal>
using namespace piscsi_interface;
using namespace piscsi_util;
void PiscsiService::Cleanup() const
{
running = false;
if (service_socket != -1) {
close(service_socket);
}
}
bool PiscsiService::Init(const callback& cb, int port)
{
if (port <= 0 || port > 65535) {
return false;
}
// Create socket for monitor
sockaddr_in server = {};
service_socket = socket(PF_INET, SOCK_STREAM, 0);
if (service_socket == -1) {
LOGERROR("Unable to create socket")
return false;
}
server.sin_family = PF_INET;
server.sin_port = htons((uint16_t)port);
server.sin_addr.s_addr = htonl(INADDR_ANY);
// Allow address reuse
if (int yes = 1; setsockopt(service_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
return false;
}
signal(SIGPIPE, SIG_IGN);
if (bind(service_socket, (sockaddr *)&server, sizeof(sockaddr_in)) < 0) {
cerr << "Error: Port " << port << " is in use, is piscsi or rascsi already running?" << endl;
return false;
}
execute = cb;
monthread = thread(&PiscsiService::Execute, this);
monthread.detach();
// Interrupt handler settings
return signal(SIGINT, KillHandler) != SIG_ERR && signal(SIGHUP, KillHandler) != SIG_ERR
&& signal(SIGTERM, KillHandler) != SIG_ERR;
}
void PiscsiService::Execute() const
{
#ifdef __linux__
// Scheduler Settings
sched_param schedparam;
schedparam.sched_priority = 0;
sched_setscheduler(0, SCHED_IDLE, &schedparam);
#endif
// Set the affinity to a specific processor core
FixCpu(2);
// Wait for the execution to start
const timespec ts = { .tv_sec = 0, .tv_nsec = 1000};
while (!running) {
nanosleep(&ts, nullptr);
}
// Set up the monitor socket to receive commands
listen(service_socket, 1);
while (true) {
CommandContext context;
try {
PbCommand command = ReadCommand(context);
if (context.IsValid()) {
execute(context, command);
}
}
catch(const io_exception& e) {
LOGWARN("%s", e.what())
// Fall through
}
context.Cleanup();
}
}
PbCommand PiscsiService::ReadCommand(CommandContext& context) const
{
// Wait for connection
sockaddr client = {};
socklen_t socklen = sizeof(client);
const int fd = accept(service_socket, &client, &socklen);
if (fd == -1) {
throw io_exception("accept() failed");
}
PbCommand command;
// Read magic string
vector<byte> magic(6);
const size_t bytes_read = context.GetSerializer().ReadBytes(fd, magic);
if (!bytes_read) {
return command;
}
if (bytes_read != magic.size() || memcmp(magic.data(), "RASCSI", magic.size())) {
throw io_exception("Invalid magic");
}
// Fetch the command
context.GetSerializer().DeserializeMessage(fd, command);
context.SetFd(fd);
return command;
}
+51
View File
@@ -0,0 +1,51 @@
//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2022 Uwe Seimet
//
//---------------------------------------------------------------------------
#pragma once
#include "generated/piscsi_interface.pb.h"
#include <functional>
#include <thread>
class CommandContext;
using namespace std;
using namespace piscsi_interface;
class PiscsiService
{
using callback = function<bool(const CommandContext&, piscsi_interface::PbCommand&)>;
callback execute;
int service_socket = -1;
thread monthread;
static inline volatile bool running = false;
public:
PiscsiService() = default;
~PiscsiService() = default;
bool Init(const callback&, int);
void Cleanup() const;
bool IsRunning() const { return running; }
void SetRunning(bool b) const { running = b; }
private:
void Execute() const;
PbCommand ReadCommand(CommandContext&) const;
static void KillHandler(int) { running = false; }
};