diff --git a/.gitignore b/.gitignore index b1dd8ef..cd152a4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,6 @@ pkg_info.json *.swp .DS_Store -# ctags +# ctags/cscope tags - +cscope.out diff --git a/binding/core.py b/binding/core.py index b8d47f6..c89020b 100644 --- a/binding/core.py +++ b/binding/core.py @@ -18,6 +18,7 @@ import os from rpc import OstinatoRpcChannel, OstinatoRpcController, RpcError import protocols.protocol_pb2 as ost_pb +import protocols.emulproto_pb2 as emul from __init__ import __version__ class DroneProxy(object): diff --git a/client/arpstatusmodel.cpp b/client/arpstatusmodel.cpp new file mode 100644 index 0000000..cd6af5d --- /dev/null +++ b/client/arpstatusmodel.cpp @@ -0,0 +1,151 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "arpstatusmodel.h" + +#include "port.h" + +#include "emulproto.pb.h" + +#include + +enum { + kIp4Address, + kMacAddress, + kStatus, + kFieldCount +}; + +static QStringList columns_ = QStringList() + << "IPv4 Address" + << "Mac Address" + << "Status"; + +ArpStatusModel::ArpStatusModel(QObject *parent) + : QAbstractTableModel(parent) +{ + port_ = NULL; + deviceIndex_ = -1; + neighbors_ = NULL; +} + +int ArpStatusModel::rowCount(const QModelIndex &parent) const +{ + if (!port_ || deviceIndex_ < 0 || parent.isValid()) + return 0; + + return port_->numArp(deviceIndex_); +} + +int ArpStatusModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return columns_.size(); +} + +QVariant ArpStatusModel::headerData( + int section, + Qt::Orientation orientation, + int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (orientation) { + case Qt::Horizontal: + return columns_[section]; + case Qt::Vertical: + return QString("%1").arg(section + 1); + default: + Q_ASSERT(false); // Unreachable + } + + return QVariant(); +} + +QVariant ArpStatusModel::data(const QModelIndex &index, int role) const +{ + QString str; + + if (!port_ || deviceIndex_ < 0 || !index.isValid()) + return QVariant(); + + int arpIdx = index.row(); + int field = index.column(); + + Q_ASSERT(arpIdx < port_->numArp(deviceIndex_)); + Q_ASSERT(field < kFieldCount); + + const OstEmul::ArpEntry &arp = neighbors_->arp(arpIdx); + + switch (field) { + case kIp4Address: + switch (role) { + case Qt::DisplayRole: + return QHostAddress(arp.ip4()).toString(); + default: + break; + } + return QVariant(); + + case kMacAddress: + switch (role) { + case Qt::DisplayRole: + return QString("%1").arg(arp.mac(), 6*2, 16, QChar('0')) + .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") + .toUpper(); + default: + break; + } + return QVariant(); + + case kStatus: + switch (role) { + case Qt::DisplayRole: + return arp.mac() ? + QString("Resolved") : QString("Failed"); + default: + break; + } + return QVariant(); + + default: + Q_ASSERT(false); // unreachable! + break; + } + + qWarning("%s: Unsupported field #%d", __FUNCTION__, field); + return QVariant(); +} + +void ArpStatusModel::setDeviceIndex(Port *port, int deviceIndex) +{ + port_ = port; + deviceIndex_ = deviceIndex; + if (port_) + neighbors_ = port_->deviceNeighbors(deviceIndex); + reset(); +} + +void ArpStatusModel::updateArpStatus() +{ + reset(); +} diff --git a/client/arpstatusmodel.h b/client/arpstatusmodel.h new file mode 100644 index 0000000..64debaa --- /dev/null +++ b/client/arpstatusmodel.h @@ -0,0 +1,55 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _ARP_STATUS_MODEL_H +#define _ARP_STATUS_MODEL_H + +#include + +class Port; +namespace OstEmul { + class DeviceNeighborList; +} + +class ArpStatusModel: public QAbstractTableModel +{ + Q_OBJECT +public: + ArpStatusModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + + void setDeviceIndex(Port *port, int deviceIndex); + +public slots: + void updateArpStatus(); + +private: + Port *port_; + int deviceIndex_; + const OstEmul::DeviceNeighborList *neighbors_; +}; + +#endif + diff --git a/client/devicegroupdialog.cpp b/client/devicegroupdialog.cpp new file mode 100644 index 0000000..f5b7f2b --- /dev/null +++ b/client/devicegroupdialog.cpp @@ -0,0 +1,355 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "devicegroupdialog.h" + +#include "port.h" +#include "spinboxdelegate.h" + +#include "emulproto.pb.h" +#include "uint128.h" + +#include +#include + +enum { kVlanId, kVlanCount, kVlanStep, kVlanCfi, kVlanPrio, kVlanTpid, + kVlanColumns }; +static QStringList vlanTableColumnHeaders = QStringList() + << "Vlan Id" << "Count" << "Step" << "CFI/DE" << "Prio" << "TPID"; + +enum { kIpNone, kIp4, kIp6, kIpDual }; +static QStringList ipStackItems = QStringList() + << "None" << "IPv4" << "IPv6" << "Dual"; + +inline UInt128 UINT128(OstEmul::Ip6Address x) +{ + return UInt128(x.hi(), x.lo()); +} + +inline OstEmul::Ip6Address IP6ADDR(UInt128 x) +{ + OstEmul::Ip6Address ip; + + ip.set_hi(x.hi64()); + ip.set_lo(x.lo64()); + return ip; +} + +DeviceGroupDialog::DeviceGroupDialog( + Port *port, + int deviceGroupIndex, + QWidget *parent, + Qt::WindowFlags flags) + : QDialog(parent, flags), port_(port), index_(deviceGroupIndex) +{ + // Setup the Dialog + setupUi(this); + + vlanTagCount->setRange(0, kMaxVlanTags); + + // Populate the Vlan Table with placeholders - we do this so that + // user entered values are retained during the lifetime of the dialog + // even if user is playing around with number of vlan tags + vlans->setRowCount(kMaxVlanTags); + vlans->setColumnCount(kVlanColumns); + vlans->setHorizontalHeaderLabels(vlanTableColumnHeaders); + for (int i = 0; i < kMaxVlanTags; i++) { + // Use same default values as defined in .proto + vlans->setItem(i, kVlanId, + new QTableWidgetItem(QString::number(100*(i+1)))); + vlans->setItem(i, kVlanCount, + new QTableWidgetItem(QString::number(1))); + vlans->setItem(i, kVlanStep, + new QTableWidgetItem(QString::number(1))); + vlans->setItem(i, kVlanCfi, + new QTableWidgetItem(QString::number(0))); + vlans->setItem(i, kVlanPrio, + new QTableWidgetItem(QString::number(0))); + vlans->setItem(i, kVlanTpid, + new QTableWidgetItem(QString("0x8100"))); + } + + // Set SpinBoxDelegate for all columns except TPID + SpinBoxDelegate *spd = new SpinBoxDelegate(this); + spd->setColumnRange(kVlanId, 0, 4095); + spd->setColumnRange(kVlanStep, 0, 4095); + spd->setColumnRange(kVlanCfi, 0, 1); + spd->setColumnRange(kVlanPrio, 0, 7); + for (int i = 0; i < kVlanColumns; i++) { + if (i != kVlanTpid) + vlans->setItemDelegateForColumn(i, spd); + } + + vlans->horizontalHeader()->setResizeMode(QHeaderView::Stretch); + vlans->resizeRowsToContents(); + + // Set vlan tag count *after* adding all items, so connected slots + // can access the items + vlanTagCount->setValue(kMaxVlanTags); + + ipStack->insertItems(0, ipStackItems); + + // TODO: DeviceGroup Traversal; hide buttons for now + // NOTE for implementation: Use a QHash + // to store modified values while traversing; in accept() + // update port->deviceGroups[] from the QHash + prev->setHidden(true); + next->setHidden(true); + + // TODO: Preview devices expanded from deviceGroup configuration + // for user convenience + + // setup dialog to auto-resize as widgets are hidden or shown + layout()->setSizeConstraint(QLayout::SetFixedSize); + + connect(devicePerVlanCount, SIGNAL(valueChanged(const QString&)), + this, SLOT(updateTotalDeviceCount())); + + connect(ip4Address, SIGNAL(textEdited(const QString&)), + this, SLOT(updateIp4Gateway())); + connect(ip4PrefixLength, SIGNAL(valueChanged(const QString&)), + this, SLOT(updateIp4Gateway())); + connect(ip6Address, SIGNAL(textEdited(const QString&)), + this, SLOT(updateIp6Gateway())); + connect(ip6PrefixLength, SIGNAL(valueChanged(const QString&)), + this, SLOT(updateIp6Gateway())); + + loadDeviceGroup(); +} + +void DeviceGroupDialog::accept() +{ + storeDeviceGroup(); + QDialog::accept(); +} + +// +// Private Slots +// +void DeviceGroupDialog::on_vlanTagCount_valueChanged(int value) +{ + Q_ASSERT((value >= 0) && (value <= kMaxVlanTags)); + + for (int row = 0; row < kMaxVlanTags; row++) + vlans->setRowHidden(row, row >= value); + + vlans->setVisible(value > 0); + updateTotalVlanCount(); +} + +void DeviceGroupDialog::on_vlans_cellChanged(int row, int col) +{ + if (col != kVlanCount) + return; + + if (vlans->isRowHidden(row)) + return; + + updateTotalVlanCount(); +} + +void DeviceGroupDialog::on_ipStack_currentIndexChanged(int index) +{ + switch (index) { + case kIpNone: + ip4->hide(); + ip6->hide(); + break; + case kIp4: + ip4->show(); + ip6->hide(); + break; + case kIp6: + ip4->hide(); + ip6->show(); + break; + case kIpDual: + ip4->show(); + ip6->show(); + break; + default: + Q_ASSERT(false); // Unreachable! + break; + } +} + +void DeviceGroupDialog::updateTotalVlanCount() +{ + int count = vlanTagCount->value() ? 1 : 0; + for (int i = 0; i < vlanTagCount->value(); i++) + count *= vlans->item(i, kVlanCount)->text().toUInt(); + vlanCount->setValue(count); + + updateTotalDeviceCount(); +} + +void DeviceGroupDialog::updateTotalDeviceCount() +{ + totalDeviceCount->setValue(qMax(vlanCount->value(), 1) + * devicePerVlanCount->value()); +} + +void DeviceGroupDialog::updateIp4Gateway() +{ + quint32 net = ip4Address->value() & (~0 << (32 - ip4PrefixLength->value())); + ip4Gateway->setValue(net | 0x01); +} + +void DeviceGroupDialog::updateIp6Gateway() +{ + UInt128 net = ip6Address->value() + & (~UInt128(0, 0) << (128 - ip6PrefixLength->value())); + ip6Gateway->setValue(net | UInt128(0, 1)); +} + +void DeviceGroupDialog::loadDeviceGroup() +{ + const OstProto::DeviceGroup *devGrp = port_->deviceGroupByIndex(index_); + int tagCount = 0; + // use 1-indexed id so that it matches the port id used in the + // RFC 4814 compliant mac addresses assigned by default to deviceGroups + // XXX: use deviceGroupId also as part of the id? + quint32 id = (port_->id()+1) & 0xff; + + Q_ASSERT(devGrp); + + name->setText(QString::fromStdString(devGrp->core().name())); + + if (devGrp->has_encap() && devGrp->encap().HasExtension(OstEmul::vlan)) { + OstEmul::VlanEmulation vlan = devGrp->encap() + .GetExtension(OstEmul::vlan); + tagCount = vlan.stack_size(); + for (int i = 0; i < tagCount; i++) { + OstEmul::VlanEmulation::Vlan v = vlan.stack(i); + vlans->item(i, kVlanPrio)->setText( + QString::number((v.vlan_tag() >> 13) & 0x7)); + vlans->item(i, kVlanCfi)->setText( + QString::number((v.vlan_tag() >> 12) & 0x1)); + vlans->item(i, kVlanId)->setText( + QString::number(v.vlan_tag() & 0x0fff)); + vlans->item(i, kVlanCount)->setText(QString::number(v.count())); + vlans->item(i, kVlanStep)->setText(QString::number(v.step())); + vlans->item(i, kVlanTpid)->setText(QString("0x%1") + .arg(v.tpid(), 0, 16)); + } + } + vlanTagCount->setValue(tagCount); + + updateTotalVlanCount(); + devicePerVlanCount->setValue(devGrp->device_count()); + + OstEmul::MacEmulation mac = devGrp->GetExtension(OstEmul::mac); + Q_ASSERT(mac.has_address()); + macAddress->setValue(mac.address()); + macStep->setValue(mac.step()); + + OstEmul::Ip4Emulation ip4 = devGrp->GetExtension(OstEmul::ip4); + // If address is not set, assign one from RFC 2544 space - 192.18.0.0/15 + // Use port Id as the 3rd octet of the address + ip4Address->setValue(ip4.has_address() ? + ip4.address() : 0xc6120002 | (id << 8)); + ip4PrefixLength->setValue(ip4.prefix_length()); + ip4Step->setValue(ip4.has_step()? ip4.step() : 1); + ip4Gateway->setValue(ip4.has_default_gateway() ? + ip4.default_gateway() : 0xc6120001 | (id << 8)); + + OstEmul::Ip6Emulation ip6 = devGrp->GetExtension(OstEmul::ip6); + // If address is not set, assign one from RFC 5180 space 2001:0200::/64 + // Use port Id as the 3rd hextet of the address + ip6Address->setValue(ip6.has_address() ? + UINT128(ip6.address()) : + UInt128((0x200102000000ULL | id) << 16, 2)); + ip6PrefixLength->setValue(ip6.prefix_length()); + ip6Step->setValue(ip6.has_step() ? UINT128(ip6.step()) : UInt128(0, 1)); + ip6Gateway->setValue(ip6.has_default_gateway() ? + UINT128(ip6.default_gateway()) : + UInt128((0x200102000000ULL | id) << 16, 1)); + + int stk = kIpNone; + if (devGrp->HasExtension(OstEmul::ip4)) + if (devGrp->HasExtension(OstEmul::ip6)) + stk = kIpDual; + else + stk = kIp4; + else if (devGrp->HasExtension(OstEmul::ip6)) + stk = kIp6; + ipStack->setCurrentIndex(stk); +} + +void DeviceGroupDialog::storeDeviceGroup() +{ + OstProto::DeviceGroup *devGrp = port_->mutableDeviceGroupByIndex(index_); + int tagCount = vlanTagCount->value(); + + Q_ASSERT(devGrp); + + devGrp->mutable_core()->set_name(name->text().toStdString()); + + OstEmul::VlanEmulation *vlan = devGrp->mutable_encap() + ->MutableExtension(OstEmul::vlan); + vlan->clear_stack(); + for (int i = 0; i < tagCount; i++) { + OstEmul::VlanEmulation::Vlan *v = vlan->add_stack(); + v->set_vlan_tag( + vlans->item(i, kVlanPrio)->text().toUInt() << 13 + | vlans->item(i, kVlanCfi)->text().toUInt() << 12 + | vlans->item(i, kVlanId)->text().toUInt()); + v->set_count(vlans->item(i, kVlanCount)->text().toUInt()); + v->set_step(vlans->item(i, kVlanStep)->text().toUInt()); + v->set_tpid(vlans->item(i, kVlanTpid)->text().toUInt(NULL, 16)); + } + + if (!tagCount) + devGrp->clear_encap(); + + devGrp->set_device_count(devicePerVlanCount->value()); + + OstEmul::MacEmulation *mac = devGrp->MutableExtension(OstEmul::mac); + mac->set_address(macAddress->value()); + mac->set_step(macStep->value()); + + if (ipStack->currentIndex() == kIp4 + || ipStack->currentIndex() == kIpDual) { + OstEmul::Ip4Emulation *ip4 = devGrp->MutableExtension(OstEmul::ip4); + ip4->set_address(ip4Address->value()); + ip4->set_prefix_length(ip4PrefixLength->value()); + ip4->set_default_gateway(ip4Gateway->value()); + ip4->set_step(ip4Step->value()); + + if (ipStack->currentIndex() == kIp4) + devGrp->ClearExtension(OstEmul::ip6); + } + + if (ipStack->currentIndex() == kIp6 + || ipStack->currentIndex() == kIpDual) { + OstEmul::Ip6Emulation *ip6 = devGrp->MutableExtension(OstEmul::ip6); + ip6->mutable_address()->CopyFrom(IP6ADDR(ip6Address->value())); + ip6->set_prefix_length(ip6PrefixLength->value()); + ip6->mutable_step()->CopyFrom(IP6ADDR(ip6Step->value())); + ip6->mutable_default_gateway()->CopyFrom(IP6ADDR(ip6Gateway->value())); + + if (ipStack->currentIndex() == kIp6) + devGrp->ClearExtension(OstEmul::ip4); + } + + if (ipStack->currentIndex() == kIpNone) { + devGrp->ClearExtension(OstEmul::ip4); + devGrp->ClearExtension(OstEmul::ip6); + } +} diff --git a/client/devicegroupdialog.h b/client/devicegroupdialog.h new file mode 100644 index 0000000..41f2358 --- /dev/null +++ b/client/devicegroupdialog.h @@ -0,0 +1,56 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICE_GROUP_DIALOG_H +#define _DEVICE_GROUP_DIALOG_H + +#include "ui_devicegroupdialog.h" + +#include + +class Port; + +class DeviceGroupDialog: public QDialog, private Ui::DeviceGroupDialog +{ + Q_OBJECT +public: + DeviceGroupDialog(Port *port, int deviceGroupIndex, + QWidget *parent = NULL, Qt::WindowFlags flags = 0); + + virtual void accept(); +private slots: + void on_vlanTagCount_valueChanged(int value); + void on_vlans_cellChanged(int row, int col); + void on_ipStack_currentIndexChanged(int index); + + void updateTotalVlanCount(); + void updateTotalDeviceCount(); + void updateIp4Gateway(); + void updateIp6Gateway(); + + void loadDeviceGroup(); + void storeDeviceGroup(); +private: + static const int kMaxVlanTags = 4; + + Port *port_; + int index_; +}; + +#endif diff --git a/client/devicegroupdialog.ui b/client/devicegroupdialog.ui new file mode 100644 index 0000000..cd9bf87 --- /dev/null +++ b/client/devicegroupdialog.ui @@ -0,0 +1,385 @@ + + DeviceGroupDialog + + + + 0 + 0 + 504 + 465 + + + + Devices + + + + + + + + Name + + + + + + + + + + Vlan Tags + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Total Vlans + + + + + + + false + + + + + + + Devices Per Vlan + + + devicePerVlanCount + + + + + + + + + + Total Devices + + + + + + + false + + + + + + + Mac Address + + + + + + + + + + Step + + + + + + + + + + IP Stack + + + + + + + + + + + + QFrame::Box + + + QFrame::Plain + + + + + + IPv4 Address + + + + + + + + + + + 0 + 0 + + + + / + + + + + + + Step + + + + + + + + + + Gateway + + + + + + + + + + 1 + + + 32 + + + + + + + + + + QFrame::Box + + + QFrame::Plain + + + + + + IPv6 Address + + + + + + + + + + + 0 + 0 + + + + / + + + + + + + Step + + + + + + + + + + Gateway + + + + + + + + + + 1 + + + 128 + + + + + + + + + + + + + + < + + + + + + + > + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + + + + MacEdit + QLineEdit +
macedit.h
+
+ + Ip4Edit + QLineEdit +
ip4edit.h
+
+ + Ip6Edit + QLineEdit +
ip6edit.h
+
+ + IntEdit + QSpinBox +
intedit.h
+
+
+ + name + vlanTagCount + vlans + devicePerVlanCount + macAddress + macStep + ipStack + ip4Address + ip4PrefixLength + ip4Step + ip4Gateway + ip6Address + ip6PrefixLength + ip6Step + ip6Gateway + prev + next + buttonBox + + + + + buttonBox + accepted() + DeviceGroupDialog + accept() + + + 227 + 289 + + + 157 + 274 + + + + + buttonBox + rejected() + DeviceGroupDialog + reject() + + + 295 + 295 + + + 286 + 274 + + + + +
diff --git a/client/devicegroupmodel.cpp b/client/devicegroupmodel.cpp new file mode 100644 index 0000000..8749d74 --- /dev/null +++ b/client/devicegroupmodel.cpp @@ -0,0 +1,282 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "devicegroupmodel.h" + +#include "port.h" + +#include "emulproto.pb.h" +#include "uint128.h" + +#include + +enum { + kName, + kVlanCount, + kDeviceCount, // Across all vlans + kIp, + kIp4Address, + kIp6Address, + kFieldCount +}; + +static QStringList columns_ = QStringList() + << "Name" + << "Vlans" + << "Devices" + << "IP Stack" + << "IPv4 Address" + << "IPv6 Address"; + +DeviceGroupModel::DeviceGroupModel(QObject *parent) + : QAbstractTableModel(parent) +{ + port_ = NULL; +} + +int DeviceGroupModel::rowCount(const QModelIndex &parent) const +{ + if (!port_ || parent.isValid()) + return 0; + + return port_->numDeviceGroups(); +} + +int DeviceGroupModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return columns_.size(); +} + +QVariant DeviceGroupModel::headerData( + int section, + Qt::Orientation orientation, + int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (orientation) { + case Qt::Horizontal: + return columns_[section]; + case Qt::Vertical: + return QString("%1").arg(section + 1); + default: + Q_ASSERT(false); // Unreachable + } + + return QVariant(); +} + +QVariant DeviceGroupModel::data(const QModelIndex &index, int role) const +{ + if (!port_ || !index.isValid()) + return QVariant(); + + int dgIdx = index.row(); + int field = index.column(); + + Q_ASSERT(dgIdx < port_->numDeviceGroups()); + Q_ASSERT(field < kFieldCount); + + const OstProto::DeviceGroup *devGrp = port_->deviceGroupByIndex(dgIdx); + + Q_ASSERT(devGrp); + + switch (field) { + case kName: + switch (role) { + case Qt::DisplayRole: + return QString::fromStdString(devGrp->core().name()); + default: + break; + } + return QVariant(); + + case kVlanCount: + switch (role) { + case Qt::DisplayRole: + if (int v = vlanCount(devGrp)) + return v; + return QString("None"); + case Qt::TextAlignmentRole: + return Qt::AlignRight; + default: + break; + } + return QVariant(); + + case kDeviceCount: + switch (role) { + case Qt::DisplayRole: + return qMax(vlanCount(devGrp), 1)*devGrp->device_count(); + case Qt::TextAlignmentRole: + return Qt::AlignRight; + default: + break; + } + return QVariant(); + + case kIp: + switch (role) { + case Qt::DisplayRole: + if (devGrp->HasExtension(OstEmul::ip4)) + if (devGrp->HasExtension(OstEmul::ip6)) + return QString("Dual Stack"); + else + return QString("IPv4"); + else if (devGrp->HasExtension(OstEmul::ip6)) + return QString("IPv6"); + else + return QString("None"); + default: + break; + } + return QVariant(); + + case kIp4Address: + switch (role) { + case Qt::DisplayRole: + if (devGrp->HasExtension(OstEmul::ip4)) + return QHostAddress( + devGrp->GetExtension(OstEmul::ip4) + .address()).toString(); + else + return QString("--"); + default: + break; + } + return QVariant(); + + case kIp6Address: + switch (role) { + case Qt::DisplayRole: + if (devGrp->HasExtension(OstEmul::ip6)) { + OstEmul::Ip6Address ip = devGrp->GetExtension( + OstEmul::ip6).address(); + return QHostAddress( + UInt128(ip.hi(), ip.lo()).toArray()) + .toString(); + } + else + return QString("--"); + default: + break; + } + return QVariant(); + + default: + Q_ASSERT(false); // unreachable! + break; + } + + qWarning("%s: Unsupported field #%d", __FUNCTION__, field); + return QVariant(); +} + +bool DeviceGroupModel::setData( + const QModelIndex &index, + const QVariant &value, + int role) +{ + if (!port_) + return false; + + // TODO; when implementing also implement flags() to + // return ItemIsEditable + return false; +} + +bool DeviceGroupModel::insertRows( + int row, + int count, + const QModelIndex &parent) +{ + int c = 0; + + Q_ASSERT(!parent.isValid()); + + beginInsertRows(parent, row, row+count-1); + for (int i = 0; i < count; i++) { + if (port_->newDeviceGroupAt(row)) + c++; + } + endInsertRows(); + + if (c != count) { + qWarning("failed to insert rows in DeviceGroupModel at row %d; " + "requested = %d, actual = %d", row, count, c); + return false; + } + + return true; +} + +bool DeviceGroupModel::removeRows( + int row, + int count, + const QModelIndex &parent) +{ + int c = 0; + + Q_ASSERT(!parent.isValid()); + + beginRemoveRows(parent, row, row+count-1); + for (int i = 0; i < count; i++) { + if (port_->deleteDeviceGroupAt(row)) + c++; + } + endRemoveRows(); + + if (c != count) { + qWarning("failed to delete rows in DeviceGroupModel at row %d; " + "requested = %d, actual = %d", row, count, c); + return false; + } + + return true; +} + +void DeviceGroupModel::setPort(Port *port) +{ + port_ = port; + reset(); +} + +// +// ---------------------- Private Methods ----------------------- +// +int DeviceGroupModel::vlanCount(const OstProto::DeviceGroup *deviceGroup) const +{ + if (!deviceGroup->has_encap() + || !deviceGroup->encap().HasExtension(OstEmul::vlan)) + return 0; + + OstEmul::VlanEmulation vlan = deviceGroup->encap() + .GetExtension(OstEmul::vlan); + int numTags = vlan.stack_size(); + int count = 1; + + for (int i = 0; i < numTags; i++) + count *= vlan.stack(i).count(); + + return count; +} diff --git a/client/devicegroupmodel.h b/client/devicegroupmodel.h new file mode 100644 index 0000000..9c0ec3f --- /dev/null +++ b/client/devicegroupmodel.h @@ -0,0 +1,59 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICE_GROUP_MODEL_H +#define _DEVICE_GROUP_MODEL_H + +#include +#include + +class Port; +namespace OstProto { + class DeviceGroup; +}; + +class DeviceGroupModel: public QAbstractTableModel +{ + Q_OBJECT +public: + DeviceGroupModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole); + bool insertRows (int row, int count, + const QModelIndex &parent = QModelIndex()); + bool removeRows (int row, int count, + const QModelIndex &parent = QModelIndex()); + + void setPort(Port *port); + +private: + int vlanCount(const OstProto::DeviceGroup *deviceGroup) const; + + Port *port_; +}; + +#endif + diff --git a/client/devicemodel.cpp b/client/devicemodel.cpp new file mode 100644 index 0000000..d917e80 --- /dev/null +++ b/client/devicemodel.cpp @@ -0,0 +1,291 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "devicemodel.h" + +#include "arpstatusmodel.h" +#include "ndpstatusmodel.h" +#include "port.h" + +#include "emulproto.pb.h" +#include "uint128.h" + +#include +#include +#include +#include + +enum { + kMacAddress, + kVlans, + kIp4Address, + kIp4Gateway, + kIp6Address, + kIp6Gateway, + kArpInfo, + kNdpInfo, + kFieldCount +}; + +static QStringList columns_ = QStringList() + << "Mac" + << "Vlans" + << "IPv4 Address" + << "IPv4 Gateway" + << "IPv6 Address" + << "IPv6 Gateway" + << "ARP" + << "NDP"; + +DeviceModel::DeviceModel(QObject *parent) + : QAbstractTableModel(parent) +{ + port_ = NULL; + arpStatusModel_ = new ArpStatusModel(this); + ndpStatusModel_ = new NdpStatusModel(this); +} + +int DeviceModel::rowCount(const QModelIndex &parent) const +{ + if (!port_ || parent.isValid()) + return 0; + + return port_->numDevices(); +} + +int DeviceModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return columns_.size(); +} + +QVariant DeviceModel::headerData( + int section, + Qt::Orientation orientation, + int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (orientation) { + case Qt::Horizontal: + return columns_[section]; + case Qt::Vertical: + return QString("%1").arg(section + 1); + default: + Q_ASSERT(false); // Unreachable + } + + return QVariant(); +} + +QVariant DeviceModel::data(const QModelIndex &index, int role) const +{ + QString str; + + if (!port_ || !index.isValid()) + return QVariant(); + + int devIdx = index.row(); + int field = index.column(); + + Q_ASSERT(devIdx < port_->numDevices()); + Q_ASSERT(field < kFieldCount); + + const OstEmul::Device *dev = port_->deviceByIndex(devIdx); + + Q_ASSERT(dev); + + switch (field) { + case kMacAddress: + switch (role) { + case Qt::DisplayRole: + return QString("%1").arg(dev->mac(), 6*2, 16, QChar('0')) + .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") + .toUpper(); + default: + break; + } + return QVariant(); + + case kVlans: + switch (role) { + case Qt::DisplayRole: + if (!dev->vlan_size()) + return QString("None"); + for (int i = 0; i < dev->vlan_size(); i++) + str.append(i == 0 ? "" : ", ") + .append(QString::number(dev->vlan(i) & 0xfff)); + return str; + default: + break; + } + return QVariant(); + + case kIp4Address: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip4_prefix_length()) + return QString("%1/%2") + .arg(QHostAddress(dev->ip4()).toString()) + .arg(dev->ip4_prefix_length()); + else + return QString("--"); + default: + break; + } + return QVariant(); + + case kIp4Gateway: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip4_prefix_length()) + return QHostAddress(dev->ip4_default_gateway()) + .toString(); + else + return QString("--"); + default: + break; + } + return QVariant(); + + case kIp6Address: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip6_prefix_length()) { + OstEmul::Ip6Address ip = dev->ip6(); + return QHostAddress( + UInt128(ip.hi(), ip.lo()).toArray()) + .toString(); + } + else + return QString("--"); + default: + break; + } + return QVariant(); + + case kIp6Gateway: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip6_prefix_length()) { + OstEmul::Ip6Address ip = dev->ip6_default_gateway(); + return QHostAddress( + UInt128(ip.hi(), ip.lo()).toArray()) + .toString(); + } + else + return QString("--"); + default: + break; + } + return QVariant(); + + case kArpInfo: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip4_prefix_length()) + return QString("%1/%2") + .arg(port_->numArpResolved(devIdx)) + .arg(port_->numArp(devIdx)); + else + return QString("--"); + default: + if (dev->has_ip4_prefix_length()) + return drillableStyle(role); + break; + } + return QVariant(); + + case kNdpInfo: + switch (role) { + case Qt::DisplayRole: + if (dev->has_ip6_prefix_length()) + return QString("%1/%2") + .arg(port_->numNdpResolved(devIdx)) + .arg(port_->numNdp(devIdx)); + else + return QString("--"); + default: + if (dev->has_ip6_prefix_length()) + return drillableStyle(role); + break; + } + return QVariant(); + + default: + Q_ASSERT(false); // unreachable! + break; + } + + qWarning("%s: Unsupported field #%d", __FUNCTION__, field); + + return QVariant(); +} + +void DeviceModel::setPort(Port *port) +{ + port_ = port; + if (port_) + connect(port_, SIGNAL(deviceInfoChanged()), SLOT(updateDeviceList())); + reset(); +} + +QAbstractItemModel* DeviceModel::detailModel(const QModelIndex &index) +{ + if (!index.isValid()) + return NULL; + + switch(index.column()) { + case kArpInfo: + arpStatusModel_->setDeviceIndex(port_, index.row()); + return arpStatusModel_; + case kNdpInfo: + ndpStatusModel_->setDeviceIndex(port_, index.row()); + return ndpStatusModel_; + default: + return NULL; + } +} + +void DeviceModel::updateDeviceList() +{ + reset(); +} + +// Style roles for drillable fields +QVariant DeviceModel::drillableStyle(int role) const +{ + QFont f; + switch (role) { + case Qt::ToolTipRole: + return QString("Click for details ..."); + case Qt::ForegroundRole: + return QBrush(QColor(Qt::blue)); + case Qt::FontRole: + f.setUnderline(true); + return f; + default: + break; + } + return QVariant(); +} + diff --git a/client/devicemodel.h b/client/devicemodel.h new file mode 100644 index 0000000..a16a77d --- /dev/null +++ b/client/devicemodel.h @@ -0,0 +1,57 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICE_MODEL_H +#define _DEVICE_MODEL_H + +#include + +class ArpStatusModel; +class NdpStatusModel; +class Port; + +class DeviceModel: public QAbstractTableModel +{ + Q_OBJECT +public: + DeviceModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + + void setPort(Port *port); + QAbstractItemModel* detailModel(const QModelIndex &index); + +public slots: + void updateDeviceList(); + +private: + QVariant drillableStyle(int role) const; + + Port *port_; + ArpStatusModel *arpStatusModel_; + NdpStatusModel *ndpStatusModel_; +}; + +#endif + diff --git a/client/deviceswidget.cpp b/client/deviceswidget.cpp new file mode 100644 index 0000000..7f84a2c --- /dev/null +++ b/client/deviceswidget.cpp @@ -0,0 +1,249 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "deviceswidget.h" + +#include "devicegroupdialog.h" +#include "port.h" +#include "portgrouplist.h" + +#include +#include + +DevicesWidget::DevicesWidget(QWidget *parent) + : QWidget(parent), portGroups_(NULL) +{ + setupUi(this); + deviceGroupList->setVisible(deviceConfig->isChecked()); + deviceList->setVisible(deviceInfo->isChecked()); + refresh->setVisible(deviceInfo->isChecked()); + deviceDetail->hide(); + + deviceGroupList->verticalHeader()->setDefaultSectionSize( + deviceGroupList->verticalHeader()->minimumSectionSize()); + deviceList->verticalHeader()->setDefaultSectionSize( + deviceList->verticalHeader()->minimumSectionSize()); + deviceDetail->verticalHeader()->setDefaultSectionSize( + deviceDetail->verticalHeader()->minimumSectionSize()); + + // Populate DeviceGroup Context Menu Actions + deviceGroupList->addAction(actionNewDeviceGroup); + deviceGroupList->addAction(actionEditDeviceGroup); + deviceGroupList->addAction(actionDeleteDeviceGroup); + + // DevicesWidget's actions is an aggegate of all sub-widget's actions + addActions(deviceGroupList->actions()); +} + +void DevicesWidget::setPortGroupList(PortGroupList *portGroups) +{ + portGroups_ = portGroups; + + deviceGroupList->setModel(portGroups_->getDeviceGroupModel()); + deviceList->setModel(portGroups_->getDeviceModel()); + + connect(portGroups_->getDeviceGroupModel(), + SIGNAL(rowsInserted(QModelIndex, int, int)), + SLOT(updateDeviceViewActions())); + connect(portGroups_->getDeviceGroupModel(), + SIGNAL(rowsRemoved(QModelIndex, int, int)), + SLOT(updateDeviceViewActions())); + + connect(deviceGroupList->selectionModel(), + SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), + SLOT(updateDeviceViewActions())); + connect(deviceGroupList->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + SLOT(updateDeviceViewActions())); + + connect(deviceList->selectionModel(), + SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), + SLOT(when_deviceList_currentChanged(const QModelIndex&))); + + // FIXME: hardcoding + deviceGroupList->resizeColumnToContents(1); // Vlan Count + deviceGroupList->resizeColumnToContents(2); // Device Count + + // FIXME: hardcoding + deviceList->resizeColumnToContents(1); // Vlan Id(s) + deviceList->resizeColumnToContents(6); // ARP Info + deviceList->resizeColumnToContents(7); // NDP Info + + updateDeviceViewActions(); +} + +void DevicesWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) { + deviceDetail->hide(); + event->accept(); + } + else + event->ignore(); +} + +void DevicesWidget::setCurrentPortIndex(const QModelIndex &portIndex) +{ + Port *port = NULL; + + if (!portGroups_) + return; + + // XXX: We assume portIndex corresponds to sourceModel, not proxyModel + if (portGroups_->isPort(portIndex)) + port = &portGroups_->port(portIndex); + + portGroups_->getDeviceGroupModel()->setPort(port); + portGroups_->getDeviceModel()->setPort(port); + currentPortIndex_ = portIndex; + + updateDeviceViewActions(); +} + +void DevicesWidget::updateDeviceViewActions() +{ + QItemSelectionModel *devSel = deviceGroupList->selectionModel(); + + if (!portGroups_) + return; + + // For some reason hasSelection() returns true even if selection size is 0 + // so additional check for size introduced + if (devSel->hasSelection() && (devSel->selection().size() > 0)) { + // If more than one non-contiguous ranges selected, + // disable "New" and "Edit" + if (devSel->selection().size() > 1) { + actionNewDeviceGroup->setDisabled(true); + actionEditDeviceGroup->setDisabled(true); + } + else { + actionNewDeviceGroup->setEnabled(true); + + // Enable "Edit" only if the single range has a single row + if (devSel->selection().at(0).height() > 1) + actionEditDeviceGroup->setDisabled(true); + else + actionEditDeviceGroup->setEnabled(true); + } + + // Delete is always enabled as long as we have a selection + actionDeleteDeviceGroup->setEnabled(true); + } + else { + qDebug("No device selection"); + if (portGroups_->isPort(currentPortIndex_)) + actionNewDeviceGroup->setEnabled(true); + else + actionNewDeviceGroup->setDisabled(true); + actionEditDeviceGroup->setDisabled(true); + actionDeleteDeviceGroup->setDisabled(true); + } +} + +// +// DeviceGroup slots +// + +void DevicesWidget::on_deviceInfo_toggled(bool checked) +{ + refresh->setVisible(checked); + deviceGroupList->setHidden(checked); + deviceList->setVisible(checked); + deviceDetail->hide(); +} + +void DevicesWidget::on_actionNewDeviceGroup_triggered() +{ + // In case nothing is selected, insert 1 row at the top + int row = 0, count = 1; + QItemSelection selection = deviceGroupList->selectionModel()->selection(); + + if (!portGroups_) + return; + + // In case we have a single range selected; insert as many rows as + // in the singe selected range before the top of the selected range + if (selection.size() == 1) { + row = selection.at(0).top(); + count = selection.at(0).height(); + } + + portGroups_->getDeviceGroupModel()->insertRows(row, count); +} + +void DevicesWidget::on_actionDeleteDeviceGroup_triggered() +{ + QModelIndex index; + + if (!portGroups_) + return; + + if (deviceGroupList->selectionModel()->hasSelection()) { + while(deviceGroupList->selectionModel()->selectedRows().size()) { + index = deviceGroupList->selectionModel()->selectedRows().at(0); + portGroups_->getDeviceGroupModel()->removeRows(index.row(), 1); + } + } +} + +void DevicesWidget::on_actionEditDeviceGroup_triggered() +{ + QItemSelection selection = deviceGroupList->selectionModel()->selection(); + + // Ensure we have only one range selected which contains only one row + if ((selection.size() == 1) && (selection.at(0).height() == 1)) + on_deviceGroupList_activated(selection.at(0).topLeft()); +} + +void DevicesWidget::on_deviceGroupList_activated(const QModelIndex &index) +{ + if (!index.isValid() || !portGroups_) + return; + + DeviceGroupDialog dgd(&portGroups_->port(currentPortIndex_), index.row()); + dgd.exec(); +} + +void DevicesWidget::on_refresh_clicked() +{ + if (!portGroups_) + return; + + Q_ASSERT(portGroups_->isPort(currentPortIndex_)); + QModelIndex curPortGroup = portGroups_->getPortModel() + ->parent(currentPortIndex_); + Q_ASSERT(curPortGroup.isValid()); + Q_ASSERT(portGroups_->isPortGroup(curPortGroup)); + + deviceDetail->hide(); + portGroups_->portGroup(curPortGroup) + .getDeviceInfo(portGroups_->port(currentPortIndex_).id()); +} + +void DevicesWidget::when_deviceList_currentChanged(const QModelIndex &index) +{ + if (!index.isValid() || !portGroups_) + return; + + QAbstractItemModel *detailModel = portGroups_->getDeviceModel() + ->detailModel(index); + + deviceDetail->setModel(detailModel); + deviceDetail->setVisible(detailModel != NULL); +} diff --git a/client/deviceswidget.h b/client/deviceswidget.h new file mode 100644 index 0000000..d5ef524 --- /dev/null +++ b/client/deviceswidget.h @@ -0,0 +1,61 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICES_WIDGET_H +#define _DEVICES_WIDGET_H + +#include "ui_deviceswidget.h" +#include + +class PortGroupList; + +class DevicesWidget: public QWidget, private Ui::DevicesWidget +{ + Q_OBJECT + +public: + DevicesWidget(QWidget *parent = NULL); + void setPortGroupList(PortGroupList *portGroups); + + virtual void keyPressEvent(QKeyEvent *event); + +public slots: + void setCurrentPortIndex(const QModelIndex &portIndex); + +private slots: + void updateDeviceViewActions(); + + void on_deviceInfo_toggled(bool checked); + + void on_actionNewDeviceGroup_triggered(); + void on_actionDeleteDeviceGroup_triggered(); + void on_actionEditDeviceGroup_triggered(); + void on_deviceGroupList_activated(const QModelIndex &index); + + void on_refresh_clicked(); + + void when_deviceList_currentChanged(const QModelIndex &index); + +private: + PortGroupList *portGroups_; + QModelIndex currentPortIndex_; +}; + +#endif + diff --git a/client/deviceswidget.ui b/client/deviceswidget.ui new file mode 100644 index 0000000..898dec0 --- /dev/null +++ b/client/deviceswidget.ui @@ -0,0 +1,138 @@ + + DevicesWidget + + + + 0 + 0 + 675 + 328 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Configuration + + + true + + + + + + + Information + + + + + + + Qt::Horizontal + + + + 131 + 23 + + + + + + + + Refresh + + + + + + + + + Qt::ActionsContextMenu + + + QFrame::StyledPanel + + + 1 + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + + 0 + 1 + + + + QAbstractItemView::SelectRows + + + + + + + QAbstractItemView::SingleSelection + + + + + + + :/icons/devicegroup_add.png + + + New Device Group + + + + + :/icons/devicegroup_delete.png + + + Delete Device Group + + + + + :/icons/devicegroup_edit.png + + + Edit Device Group + + + + + + + + diff --git a/client/icons/devicegroup_add.png b/client/icons/devicegroup_add.png new file mode 100644 index 0000000..fac186b Binary files /dev/null and b/client/icons/devicegroup_add.png differ diff --git a/client/icons/devicegroup_delete.png b/client/icons/devicegroup_delete.png new file mode 100644 index 0000000..3a8c373 Binary files /dev/null and b/client/icons/devicegroup_delete.png differ diff --git a/client/icons/devicegroup_edit.png b/client/icons/devicegroup_edit.png new file mode 100644 index 0000000..eb06df3 Binary files /dev/null and b/client/icons/devicegroup_edit.png differ diff --git a/client/icons/neighbor_clear.png b/client/icons/neighbor_clear.png new file mode 100644 index 0000000..d9eefc2 Binary files /dev/null and b/client/icons/neighbor_clear.png differ diff --git a/client/icons/neighbor_resolve.png b/client/icons/neighbor_resolve.png new file mode 100644 index 0000000..ff803be Binary files /dev/null and b/client/icons/neighbor_resolve.png differ diff --git a/client/main.cpp b/client/main.cpp index 65864f0..34f689e 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -25,8 +25,10 @@ along with this program. If not, see #include "settings.h" #include +#include #include #include +#include #include @@ -68,6 +70,7 @@ int main(int argc, char* argv[]) appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString()); Preferences::initDefaults(); + qsrand(QDateTime::currentDateTime().toTime_t()); mainWindow = new MainWindow; mainWindow->show(); diff --git a/client/mainwindow.cpp b/client/mainwindow.cpp index 2f459aa..b7032d2 100644 --- a/client/mainwindow.cpp +++ b/client/mainwindow.cpp @@ -27,12 +27,18 @@ along with this program. If not, see #include "portstatswindow.h" #include "portswindow.h" #include "preferences.h" +#include "sessionfileformat.h" #include "settings.h" #include "ui_about.h" #include "updater.h" +#include "fileformat.pb.h" + #include +#include +#include #include +#include extern const char* version; extern const char* revision; @@ -75,7 +81,7 @@ MainWindow::MainWindow(QWidget *parent) setupUi(this); - menuFile->insertActions(menuFile->actions().at(0), portsWindow->actions()); + menuFile->insertActions(menuFile->actions().at(3), portsWindow->actions()); statsDock->setWidget(statsWindow); addDockWidget(Qt::BottomDockWidgetArea, statsDock); @@ -133,6 +139,102 @@ MainWindow::~MainWindow() delete localServer_; } +void MainWindow::on_actionOpenSession_triggered() +{ + qDebug("Open Session Action"); + + static QString dirName; + QString fileName; + QStringList fileTypes = SessionFileFormat::supportedFileTypes( + SessionFileFormat::kOpenFile); + QString fileType; + QString errorStr; + bool ret; + + if (portsWindow->portGroupCount()) { + if (QMessageBox::question(this, + tr("Open Session"), + tr("Existing session will be lost. Proceed?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::No) + goto _exit; + } + + if (fileTypes.size()) + fileType = fileTypes.at(0); + + fileName = QFileDialog::getOpenFileName(this, tr("Open Session"), + dirName, fileTypes.join(";;"), &fileType); + if (fileName.isEmpty()) + goto _exit; + + ret = openSession(fileName, errorStr); + if (!ret || !errorStr.isEmpty()) { + QMessageBox msgBox(this); + QStringList str = errorStr.split("\n\n\n\n"); + + msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical); + msgBox.setWindowTitle(qApp->applicationName()); + msgBox.setText(str.at(0)); + if (str.size() > 1) + msgBox.setDetailedText(str.at(1)); + msgBox.setStandardButtons(QMessageBox::Ok); + + msgBox.exec(); + } + dirName = QFileInfo(fileName).absolutePath(); + +_exit: + return; +} + +void MainWindow::on_actionSaveSession_triggered() +{ + qDebug("Save Session Action"); + + static QString fileName; + QStringList fileTypes = SessionFileFormat::supportedFileTypes( + SessionFileFormat::kSaveFile); + QString fileType; + QString errorStr; + QFileDialog::Options options; + + if (portsWindow->reservedPortCount()) { + QString myself = appSettings->value(kUserKey, kUserDefaultValue) + .toString(); + if (QMessageBox::question(this, + tr("Save Session"), + QString("Some ports are reserved!\n\nOnly ports reserved by %1 will be saved. Proceed?").arg(myself), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::No) + goto _exit; + } + + // On Mac OS with Native Dialog, getSaveFileName() ignores fileType. + // Although currently there's only one supported file type, we may + // have more in the future +#if defined(Q_OS_MAC) + options |= QFileDialog::DontUseNativeDialog; +#endif + + if (fileTypes.size()) + fileType = fileTypes.at(0); + + fileName = QFileDialog::getSaveFileName(this, tr("Save Session"), + fileName, fileTypes.join(";;"), &fileType, options); + if (fileName.isEmpty()) + goto _exit; + + if (!saveSession(fileName, fileType, errorStr)) + QMessageBox::critical(this, qApp->applicationName(), errorStr); + else if (!errorStr.isEmpty()) + QMessageBox::warning(this, qApp->applicationName(), errorStr); + + fileName = QFileInfo(fileName).absolutePath(); +_exit: + return; +} + void MainWindow::on_actionPreferences_triggered() { Preferences *preferences = new Preferences(); @@ -172,3 +274,120 @@ void MainWindow::onNewVersion(QString newVersion) statusBar()->showMessage(QString("New Ostinato version %1 available. " "Visit http://ostinato.org to download").arg(newVersion)); } + +//! Returns true on success (or user cancel) and false on failure +bool MainWindow::openSession(QString fileName, QString &error) +{ + bool ret = false; + QDialog *optDialog; + QProgressDialog progress("Opening Session", "Cancel", 0, 0, this); + OstProto::SessionContent session; + SessionFileFormat *fmt = SessionFileFormat::fileFormatFromFile(fileName); + + if (fmt == NULL) { + error = tr("Unknown session file format"); + goto _fail; + } + + if ((optDialog = fmt->openOptionsDialog())) + { + int ret; + optDialog->setParent(this, Qt::Dialog); + ret = optDialog->exec(); + optDialog->setParent(0, Qt::Dialog); + if (ret == QDialog::Rejected) + goto _user_opt_cancel; + } + + progress.setAutoReset(false); + progress.setAutoClose(false); + progress.setMinimumDuration(0); + progress.show(); + + setDisabled(true); + progress.setEnabled(true); // to override the mainWindow disable + + connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); + connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); + connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); + connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); + + fmt->openAsync(fileName, session, error); + qDebug("after open async"); + + while (!fmt->isFinished()) + qApp->processEvents(); + qDebug("wait over for async operation"); + + if (!fmt->result()) + goto _fail; + + // process any remaining events posted from the thread + for (int i = 0; i < 10; i++) + qApp->processEvents(); + + // XXX: user can't cancel operation from here on! + progress.close(); + + portsWindow->openSession(&session, error); + +_user_opt_cancel: + ret = true; + +_fail: + progress.close(); + setEnabled(true); + return ret; +} + +bool MainWindow::saveSession(QString fileName, QString fileType, QString &error) +{ + bool ret = false; + QProgressDialog progress("Saving Session", "Cancel", 0, 0, this); + SessionFileFormat *fmt = SessionFileFormat::fileFormatFromType(fileType); + OstProto::SessionContent session; + + if (fmt == NULL) + goto _fail; + + progress.setAutoReset(false); + progress.setAutoClose(false); + progress.setMinimumDuration(0); + progress.show(); + + setDisabled(true); + progress.setEnabled(true); // to override the mainWindow disable + + // Fill in session + ret = portsWindow->saveSession(&session, error, &progress); + if (!ret) + goto _user_cancel; + + connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); + connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); + connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); + connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); + + fmt->saveAsync(session, fileName, error); + qDebug("after save async"); + + while (!fmt->isFinished()) + qApp->processEvents(); + qDebug("wait over for async operation"); + + ret = fmt->result(); + goto _exit; + +_user_cancel: + goto _exit; + +_fail: + error = QString("Unsupported File Type - %1").arg(fileType); + goto _exit; + +_exit: + progress.close(); + setEnabled(true); + return ret; +} + diff --git a/client/mainwindow.h b/client/mainwindow.h index e05ab54..fa575f3 100644 --- a/client/mainwindow.h +++ b/client/mainwindow.h @@ -34,6 +34,9 @@ class MainWindow : public QMainWindow, private Ui::MainWindow Q_OBJECT private: + bool openSession(QString fileName, QString &error); + bool saveSession(QString fileName, QString fileType, QString &error); + QProcess *localServer_; PortsWindow *portsWindow; PortStatsWindow *statsWindow; @@ -48,6 +51,8 @@ public: ~MainWindow(); public slots: + void on_actionOpenSession_triggered(); + void on_actionSaveSession_triggered(); void on_actionPreferences_triggered(); void on_actionViewRestoreDefaults_triggered(); void on_actionHelpAbout_triggered(); diff --git a/client/mainwindow.ui b/client/mainwindow.ui index 82dfc93..6e79e3d 100644 --- a/client/mainwindow.ui +++ b/client/mainwindow.ui @@ -5,8 +5,8 @@ 0 0 - 700 - 550 + 1024 + 600 @@ -17,18 +17,12 @@ - - - 0 - 0 - 700 - 21 - - &File + + @@ -97,6 +91,16 @@ Restore &Defaults + + + Open Session ... + + + + + Save Session ... + + diff --git a/client/ndpstatusmodel.cpp b/client/ndpstatusmodel.cpp new file mode 100644 index 0000000..4a48694 --- /dev/null +++ b/client/ndpstatusmodel.cpp @@ -0,0 +1,155 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "ndpstatusmodel.h" + +#include "port.h" + +#include "emulproto.pb.h" + +#include "uint128.h" +#include + +enum { + kIp4Address, + kMacAddress, + kStatus, + kFieldCount +}; + +static QStringList columns_ = QStringList() + << "IPv6 Address" + << "Mac Address" + << "Status"; + +NdpStatusModel::NdpStatusModel(QObject *parent) + : QAbstractTableModel(parent) +{ + port_ = NULL; + deviceIndex_ = -1; + neighbors_ = NULL; +} + +int NdpStatusModel::rowCount(const QModelIndex &parent) const +{ + if (!port_ || deviceIndex_ < 0 || parent.isValid()) + return 0; + + return port_->numNdp(deviceIndex_); +} + +int NdpStatusModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return columns_.size(); +} + +QVariant NdpStatusModel::headerData( + int section, + Qt::Orientation orientation, + int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (orientation) { + case Qt::Horizontal: + return columns_[section]; + case Qt::Vertical: + return QString("%1").arg(section + 1); + default: + Q_ASSERT(false); // Unreachable + } + + return QVariant(); +} + +QVariant NdpStatusModel::data(const QModelIndex &index, int role) const +{ + QString str; + + if (!port_ || deviceIndex_ < 0 || !index.isValid()) + return QVariant(); + + int ndpIdx = index.row(); + int field = index.column(); + + Q_ASSERT(ndpIdx < port_->numNdp(deviceIndex_)); + Q_ASSERT(field < kFieldCount); + + const OstEmul::NdpEntry &ndp = neighbors_->ndp(ndpIdx); + + switch (field) { + case kIp4Address: + switch (role) { + case Qt::DisplayRole: + return QHostAddress( + UInt128(ndp.ip6().hi(), + ndp.ip6().lo()).toArray()) + .toString(); + default: + break; + } + return QVariant(); + + case kMacAddress: + switch (role) { + case Qt::DisplayRole: + return QString("%1").arg(ndp.mac(), 6*2, 16, QChar('0')) + .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") + .toUpper(); + default: + break; + } + return QVariant(); + + case kStatus: + switch (role) { + case Qt::DisplayRole: + return ndp.mac() ? + QString("Resolved") : QString("Failed"); + default: + break; + } + return QVariant(); + + default: + Q_ASSERT(false); // unreachable! + break; + } + + qWarning("%s: Unsupported field #%d", __FUNCTION__, field); + return QVariant(); +} + +void NdpStatusModel::setDeviceIndex(Port *port, int deviceIndex) +{ + port_ = port; + deviceIndex_ = deviceIndex; + if (port_) + neighbors_ = port_->deviceNeighbors(deviceIndex); + reset(); +} + +void NdpStatusModel::updateNdpStatus() +{ + reset(); +} diff --git a/client/ndpstatusmodel.h b/client/ndpstatusmodel.h new file mode 100644 index 0000000..c00c6b0 --- /dev/null +++ b/client/ndpstatusmodel.h @@ -0,0 +1,55 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _NDP_STATUS_MODEL_H +#define _NDP_STATUS_MODEL_H + +#include + +class Port; +namespace OstEmul { + class DeviceNeighborList; +} + +class NdpStatusModel: public QAbstractTableModel +{ + Q_OBJECT +public: + NdpStatusModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role) const; + + void setDeviceIndex(Port *port, int deviceIndex); + +public slots: + void updateNdpStatus(); + +private: + Port *port_; + int deviceIndex_; + const OstEmul::DeviceNeighborList *neighbors_; +}; + +#endif + diff --git a/client/ostinato.pro b/client/ostinato.pro index 374f663..e644f62 100644 --- a/client/ostinato.pro +++ b/client/ostinato.pro @@ -33,9 +33,15 @@ LIBS += -lprotobuf LIBS += -L"../extra/qhexedit2/$(OBJECTS_DIR)/" -lqhexedit2 RESOURCES += ostinato.qrc HEADERS += \ + arpstatusmodel.h \ + devicegroupdialog.h \ + devicegroupmodel.h \ + devicemodel.h \ + deviceswidget.h \ dumpview.h \ hexlineedit.h \ mainwindow.h \ + ndpstatusmodel.h \ packetmodel.h \ port.h \ portconfigdialog.h \ @@ -57,6 +63,8 @@ HEADERS += \ FORMS += \ about.ui \ + devicegroupdialog.ui \ + deviceswidget.ui \ mainwindow.ui \ portconfigdialog.ui \ portstatsfilter.ui \ @@ -67,11 +75,17 @@ FORMS += \ variablefieldswidget.ui SOURCES += \ + arpstatusmodel.cpp \ + devicegroupdialog.cpp \ + devicegroupmodel.cpp \ + devicemodel.cpp \ + deviceswidget.cpp \ dumpview.cpp \ stream.cpp \ hexlineedit.cpp \ main.cpp \ mainwindow.cpp \ + ndpstatusmodel.cpp \ packetmodel.cpp \ port.cpp \ portconfigdialog.cpp \ diff --git a/client/ostinato.qrc b/client/ostinato.qrc index d2e81f2..495626b 100644 --- a/client/ostinato.qrc +++ b/client/ostinato.qrc @@ -15,11 +15,16 @@ icons/control_stop.png icons/deco_exclusive.png icons/delete.png + icons/devicegroup_add.png + icons/devicegroup_delete.png + icons/devicegroup_edit.png icons/exit.png icons/gaps.png icons/logo.png icons/magnifier.png icons/name.png + icons/neighbor_clear.png + icons/neighbor_resolve.png icons/portgroup_add.png icons/portgroup_connect.png icons/portgroup_delete.png diff --git a/client/port.cpp b/client/port.cpp index 9542d67..50f1716 100644 --- a/client/port.cpp +++ b/client/port.cpp @@ -19,7 +19,8 @@ along with this program. If not, see #include "port.h" -#include "abstractfileformat.h" +#include "emulation.h" +#include "streamfileformat.h" #include #include @@ -31,11 +32,21 @@ along with this program. If not, see extern QMainWindow *mainWindow; uint Port::mAllocStreamId = 0; +uint Port::allocDeviceGroupId_ = 1; static const int kEthOverhead = 20; uint Port::newStreamId() { + // We use a monotonically increasing class variable to generate + // unique id. To ensure that we take into account the already + // existing ids at drone, we update this variable to be greater + // than the last id that we fetched + // FIXME: In some cases e.g. wrap around or more likely multiple + // clients, we will still run into trouble - to fix this we should + // check here that the same id does not already exist; but currently + // we can only do a linear search - fix this when the lookup/search + // is optimized return mAllocStreamId++; } @@ -55,6 +66,11 @@ Port::~Port() delete mStreams.takeFirst(); } +void Port::protoDataCopyInto(OstProto::Port *data) +{ + data->CopyFrom(d); +} + void Port::updatePortConfig(OstProto::Port *port) { bool recalc = false; @@ -65,6 +81,10 @@ void Port::updatePortConfig(OstProto::Port *port) d.MergeFrom(*port); + // Setup a user-friendly alias for Win32 ports + if (name().startsWith("\\Device\\NPF_")) + setAlias(QString("if%1").arg(id())); + if (recalc) recalculateAverageRates(); } @@ -408,6 +428,70 @@ void Port::getModifiedStreamsSinceLastSync( qDebug("Done %s", __FUNCTION__); } +void Port::getDeletedDeviceGroupsSinceLastSync( + OstProto::DeviceGroupIdList &deviceGroupIdList) +{ + deviceGroupIdList.clear_device_group_id(); + foreach(int id, lastSyncDeviceGroupList_) { + if (!deviceGroupById(id)) + deviceGroupIdList.add_device_group_id()->set_id(id); + } +} + +void Port::getNewDeviceGroupsSinceLastSync( + OstProto::DeviceGroupIdList &deviceGroupIdList) +{ + deviceGroupIdList.clear_device_group_id(); + foreach(OstProto::DeviceGroup *dg, deviceGroups_) { + quint32 dgid = dg->device_group_id().id(); + if (!lastSyncDeviceGroupList_.contains(dgid)) + deviceGroupIdList.add_device_group_id()->set_id(dgid); + } +} + +void Port::getModifiedDeviceGroupsSinceLastSync( + OstProto::DeviceGroupConfigList &deviceGroupConfigList) +{ + deviceGroupConfigList.clear_device_group(); + foreach(quint32 id, modifiedDeviceGroupList_) + deviceGroupConfigList.add_device_group() + ->CopyFrom(*deviceGroupById(id)); +} + +/*! + * Finds the user modifiable fields in 'config' that are different from + * the current configuration on the port and modifes 'config' such that + * only those fields are set and returns true. If 'config' is same as + * current port config, 'config' is unmodified and false is returned + */ +bool Port::modifiablePortConfig(OstProto::Port &config) const +{ + bool change = false; + OstProto::Port modCfg; + + if (config.is_exclusive_control() != d.is_exclusive_control()) { + modCfg.set_is_exclusive_control(config.is_exclusive_control()); + change = true; + } + if (config.transmit_mode() != d.transmit_mode()) { + modCfg.set_transmit_mode(config.transmit_mode()); + change = true; + } + if (config.user_name() != d.user_name()) { + modCfg.set_user_name(config.user_name()); + change = true; + } + + if (change) { + modCfg.mutable_port_id()->set_id(id()); + config.CopyFrom(modCfg); + + return true; + } + + return false; +} + void Port::when_syncComplete() { //reorderStreamsByOrdinals(); @@ -415,6 +499,13 @@ void Port::when_syncComplete() mLastSyncStreamList.clear(); for (int i=0; iid()); + + lastSyncDeviceGroupList_.clear(); + for (int i = 0; i < deviceGroups_.size(); i++) { + lastSyncDeviceGroupList_.append( + deviceGroups_.at(i)->device_group_id().id()); + } + modifiedDeviceGroupList_.clear(); } void Port::updateStats(OstProto::PortStats *portStats) @@ -462,10 +553,12 @@ bool Port::openStreams(QString fileName, bool append, QString &error) QDialog *optDialog; QProgressDialog progress("Opening Streams", "Cancel", 0, 0, mainWindow); OstProto::StreamConfigList streams; - AbstractFileFormat *fmt = AbstractFileFormat::fileFormatFromFile(fileName); + StreamFileFormat *fmt = StreamFileFormat::fileFormatFromFile(fileName); - if (fmt == NULL) + if (fmt == NULL) { + error = tr("Unknown streams file format"); goto _fail; + } if ((optDialog = fmt->openOptionsDialog())) { @@ -490,12 +583,12 @@ bool Port::openStreams(QString fileName, bool append, QString &error) connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); - fmt->openStreamsOffline(fileName, streams, error); - qDebug("after open offline"); + fmt->openAsync(fileName, streams, error); + qDebug("after open async"); while (!fmt->isFinished()) qApp->processEvents(); - qDebug("wait over for offline operation"); + qDebug("wait over for async operation"); if (!fmt->result()) goto _fail; @@ -549,7 +642,7 @@ bool Port::saveStreams(QString fileName, QString fileType, QString &error) { bool ret = false; QProgressDialog progress("Saving Streams", "Cancel", 0, 0, mainWindow); - AbstractFileFormat *fmt = AbstractFileFormat::fileFormatFromType(fileType); + StreamFileFormat *fmt = StreamFileFormat::fileFormatFromType(fileType); OstProto::StreamConfigList streams; if (fmt == NULL) @@ -583,12 +676,12 @@ bool Port::saveStreams(QString fileName, QString fileType, QString &error) qApp->processEvents(); } - fmt->saveStreamsOffline(streams, fileName, error); - qDebug("after save offline"); + fmt->saveAsync(streams, fileName, error); + qDebug("after save async"); while (!fmt->isFinished()) qApp->processEvents(); - qDebug("wait over for offline operation"); + qDebug("wait over for async operation"); ret = fmt->result(); goto _exit; @@ -605,3 +698,243 @@ _exit: mainWindow->setEnabled(true); return ret; } + +// ------------ Device Group ----------- // + +uint Port::newDeviceGroupId() +{ + // We use a monotonically increasing class variable to generate + // unique id. To ensure that we take into account the already + // existing ids at drone, we update this variable to be greater + // than the last id that we fetched + // FIXME: In some cases e.g. wrap around or more likely multiple + // clients, we will still run into trouble - to fix this we should + // check here that the same id does not already exist; but currently + // we can only do a linear search - fix this when the lookup/search + // is optimized + return allocDeviceGroupId_++; +} + +int Port::numDeviceGroups() const +{ + return deviceGroups_.size(); +} + +const OstProto::DeviceGroup* Port::deviceGroupByIndex(int index) const +{ + if ((index < 0) || (index >= numDeviceGroups())) { + qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, + index, numDeviceGroups() - 1); + return NULL; + } + + return deviceGroups_.at(index); +} + +OstProto::DeviceGroup* Port::mutableDeviceGroupByIndex(int index) +{ + if ((index < 0) || (index >= numDeviceGroups())) { + qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, + index, numDeviceGroups() - 1); + return NULL; + } + + OstProto::DeviceGroup *devGrp = deviceGroups_.at(index); + + // Caller can modify DeviceGroup - assume she will + modifiedDeviceGroupList_.insert(devGrp->device_group_id().id()); + + return devGrp; +} + +OstProto::DeviceGroup* Port::deviceGroupById(uint deviceGroupId) +{ + for (int i = 0; i < deviceGroups_.size(); i++) { + OstProto::DeviceGroup *devGrp = deviceGroups_.at(i); + if (devGrp->device_group_id().id() == deviceGroupId) + return devGrp; + } + + return NULL; +} + +bool Port::newDeviceGroupAt(int index, const OstProto::DeviceGroup *deviceGroup) +{ + if (index < 0 || index > numDeviceGroups()) + return false; + + OstProto::DeviceGroup *devGrp = newDeviceGroup(id()); + + if (!devGrp) { + qWarning("failed allocating a new device group"); + return false; + } + + if (deviceGroup) + devGrp->CopyFrom(*deviceGroup); + + devGrp->mutable_device_group_id()->set_id(newDeviceGroupId()); + deviceGroups_.insert(index, devGrp); + modifiedDeviceGroupList_.insert(devGrp->device_group_id().id()); + + return true; +} + +bool Port::deleteDeviceGroupAt(int index) +{ + if (index < 0 || index >= deviceGroups_.size()) + return false; + + OstProto::DeviceGroup *devGrp = deviceGroups_.takeAt(index); + modifiedDeviceGroupList_.remove(devGrp->device_group_id().id()); + delete devGrp; + + return true; +} + +bool Port::insertDeviceGroup(uint deviceGroupId) +{ + OstProto::DeviceGroup *devGrp; + + if (deviceGroupById(deviceGroupId)) { + qDebug("%s: deviceGroup id %u already exists", __FUNCTION__, + deviceGroupId); + return false; + } + + devGrp = newDeviceGroup(id()); + devGrp->mutable_device_group_id()->set_id(deviceGroupId); + deviceGroups_.append(devGrp); + + // Update allocDeviceGroupId_ to take into account the deviceGroup id + // received from server - this is required to make sure newly allocated + // deviceGroup ids are unique + if (allocDeviceGroupId_ <= deviceGroupId) + allocDeviceGroupId_ = deviceGroupId + 1; + return true; +} + +bool Port::updateDeviceGroup( + uint deviceGroupId, + OstProto::DeviceGroup *deviceGroup) +{ + OstProto::DeviceGroup *devGrp = deviceGroupById(deviceGroupId); + + if (!devGrp) { + qDebug("%s: deviceGroup id %u does not exist", __FUNCTION__, + deviceGroupId); + return false; + } + + devGrp->CopyFrom(*deviceGroup); + return true; +} + +// ------------ Devices ----------- // + +int Port::numDevices() +{ + return devices_.size(); +} + +const OstEmul::Device* Port::deviceByIndex(int index) +{ + if ((index < 0) || (index >= numDevices())) { + qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, + index, numDevices() - 1); + return NULL; + } + + return devices_.at(index); +} + +void Port::clearDeviceList() +{ + while (devices_.size()) + delete devices_.takeFirst(); +} + +void Port::insertDevice(const OstEmul::Device &device) +{ + OstEmul::Device *dev = new OstEmul::Device(device); + devices_.append(dev); +} + +// ------------- Device Neighbors (ARP/NDP) ------------- // + +const OstEmul::DeviceNeighborList* Port::deviceNeighbors(int deviceIndex) +{ + if ((deviceIndex < 0) || (deviceIndex >= numDevices())) { + qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, + deviceIndex, numDevices() - 1); + return NULL; + } + + return deviceNeighbors_.value(deviceIndex); +} + +int Port::numArp(int deviceIndex) +{ + if (deviceNeighbors_.contains(deviceIndex)) + return deviceNeighbors_.value(deviceIndex)->arp_size(); + + return 0; +} + +int Port::numArpResolved(int deviceIndex) +{ + if (arpResolvedCount_.contains(deviceIndex)) + return arpResolvedCount_.value(deviceIndex); + + return 0; +} + +int Port::numNdp(int deviceIndex) +{ + if (deviceNeighbors_.contains(deviceIndex)) + return deviceNeighbors_.value(deviceIndex)->ndp_size(); + + return 0; +} + +int Port::numNdpResolved(int deviceIndex) +{ + if (ndpResolvedCount_.contains(deviceIndex)) + return ndpResolvedCount_.value(deviceIndex); + + return 0; +} + +void Port::clearDeviceNeighbors() +{ + arpResolvedCount_.clear(); + ndpResolvedCount_.clear(); + qDeleteAll(deviceNeighbors_); + deviceNeighbors_.clear(); +} + +void Port::insertDeviceNeighbors(const OstEmul::DeviceNeighborList &neighList) +{ + int count; + OstEmul::DeviceNeighborList *neighbors = + new OstEmul::DeviceNeighborList(neighList); + deviceNeighbors_.insert(neighList.device_index(), neighbors); + + count = 0; + for (int i = 0; i < neighbors->arp_size(); i++) + if (neighbors->arp(i).mac()) + count++; + arpResolvedCount_.insert(neighbors->device_index(), count); + + count = 0; + for (int i = 0; i < neighbors->ndp_size(); i++) + if (neighbors->ndp(i).mac()) + count++; + ndpResolvedCount_.insert(neighbors->device_index(), count); +} + +void Port::deviceInfoRefreshed() +{ + emit deviceInfoChanged(); +} + diff --git a/client/port.h b/client/port.h index 006ba7e..8496843 100644 --- a/client/port.h +++ b/client/port.h @@ -21,19 +21,26 @@ along with this program. If not, see #define _PORT_H #include +#include #include +#include #include #include #include "stream.h" //class StreamModel; +namespace OstEmul { + class Device; + class DeviceNeighborList; +} class Port : public QObject { Q_OBJECT static uint mAllocStreamId; + static uint allocDeviceGroupId_; OstProto::Port d; OstProto::PortStats stats; @@ -51,6 +58,14 @@ class Port : public QObject { QList mLastSyncStreamList; QList mStreams; // sorted by stream's ordinal value + QList lastSyncDeviceGroupList_; + QSet modifiedDeviceGroupList_; + QList deviceGroups_; + QList devices_; + QHash deviceNeighbors_; + QHash arpResolvedCount_; + QHash ndpResolvedCount_; + uint newStreamId(); void updateStreamOrdinalsFromIndex(); void reorderStreamsByOrdinals(); @@ -64,7 +79,8 @@ public: ~Port(); quint32 portGroupId() const { return mPortGroupId; } - const QString& userAlias() const { return mUserAlias; } + const QString userAlias() const + { return mUserAlias.isEmpty() ? name() : mUserAlias; } quint32 id() const { return d.port_id().id(); } @@ -88,7 +104,7 @@ public: { return avgBitsPerSec_; } //void setAdminEnable(AdminStatus status) { mAdminStatus = status; } - void setAlias(QString &alias) { mUserAlias = alias; } + void setAlias(QString alias) { mUserAlias = alias; } //void setExclusive(bool flag); int numStreams() { return mStreams.size(); } @@ -111,6 +127,8 @@ public: return capFile_; } + void protoDataCopyInto(OstProto::Port *data); + // FIXME(MED): naming inconsistency - PortConfig/Stream; also retVal void updatePortConfig(OstProto::Port *port); @@ -131,6 +149,15 @@ public: void getModifiedStreamsSinceLastSync( OstProto::StreamConfigList &streamConfigList); + void getDeletedDeviceGroupsSinceLastSync( + OstProto::DeviceGroupIdList &streamIdList); + void getNewDeviceGroupsSinceLastSync( + OstProto::DeviceGroupIdList &streamIdList); + void getModifiedDeviceGroupsSinceLastSync( + OstProto::DeviceGroupConfigList &streamConfigList); + + bool modifiablePortConfig(OstProto::Port &config) const; + void when_syncComplete(); void setAveragePacketRate(double packetsPerSec); @@ -145,10 +172,54 @@ public: bool openStreams(QString fileName, bool append, QString &error); bool saveStreams(QString fileName, QString fileType, QString &error); + // ------------ Device Group ----------- // + + uint newDeviceGroupId(); + int numDeviceGroups() const; + const OstProto::DeviceGroup* deviceGroupByIndex(int index) const; + OstProto::DeviceGroup* mutableDeviceGroupByIndex(int index); + OstProto::DeviceGroup* deviceGroupById(uint deviceGroupId); + + //! Used by StreamModel + //@{ + bool newDeviceGroupAt(int index, + const OstProto::DeviceGroup *deviceGroup = NULL); + bool deleteDeviceGroupAt(int index); + //@} + + //! Used by MyService::Stub to update from config received from server + //@{ + bool insertDeviceGroup(uint deviceGroupId); + bool updateDeviceGroup(uint deviceGroupId, + OstProto::DeviceGroup *deviceGroup); + //@} + + // ------------ Device ----------- // + + int numDevices(); + const OstEmul::Device* deviceByIndex(int index); + + //! Used by MyService::Stub to update from config received from server + void clearDeviceList(); + void insertDevice(const OstEmul::Device &device); + + const OstEmul::DeviceNeighborList* deviceNeighbors(int deviceIndex); + int numArp(int deviceIndex); + int numArpResolved(int deviceIndex); + int numNdp(int deviceIndex); + int numNdpResolved(int deviceIndex); + + //! Used by MyService::Stub to update from config received from server + void clearDeviceNeighbors(); + void insertDeviceNeighbors(const OstEmul::DeviceNeighborList &neighList); + + void deviceInfoRefreshed(); + signals: void portRateChanged(int portGroupId, int portId); void portDataChanged(int portGroupId, int portId); void streamListChanged(int portGroupId, int portId); + void deviceInfoChanged(); }; diff --git a/client/portgroup.cpp b/client/portgroup.cpp index 1742fe3..d81767c 100644 --- a/client/portgroup.cpp +++ b/client/portgroup.cpp @@ -21,6 +21,9 @@ along with this program. If not, see #include "settings.h" +#include "emulproto.pb.h" +#include "fileformat.pb.h" + #include #include #include @@ -48,6 +51,8 @@ PortGroup::PortGroup(QString serverName, quint16 port) statsController = new PbRpcController(portIdList_, portStatsList_); isGetStatsPending_ = false; + atConnectConfig_ = NULL; + compat = kUnknown; reconnect = false; @@ -89,8 +94,39 @@ PortGroup::~PortGroup() delete serviceStub; delete rpcChannel; delete statsController; + delete atConnectConfig_; } +void PortGroup::setConfigAtConnect(const OstProto::PortGroupContent *config) +{ + if (!config) { + delete atConnectConfig_; + atConnectConfig_ = NULL; + return; + } + + if (!atConnectConfig_) + atConnectConfig_ = new OstProto::PortGroupContent; + atConnectConfig_->CopyFrom(*config); +} + +int PortGroup::numReservedPorts() const +{ + int count = 0; + for (int i = 0; i < mPorts.size(); i++) + { + if (!mPorts[i]->userName().isEmpty()) + count++; + } + + return count; +} + +const QString PortGroup::serverFullName() const +{ + return serverPort() == DEFAULT_SERVER_PORT ? + serverName() : QString("%1:%2").arg(serverName()).arg(serverPort()); +} // ------------------------------------------------ // Slots @@ -187,6 +223,7 @@ void PortGroup::on_rpcChannel_disconnected() while (!mPorts.isEmpty()) delete mPorts.takeFirst(); + atConnectPortConfig_.clear(); emit portListChanged(mPortGroupId); emit portGroupDataChanged(mPortGroupId); @@ -304,6 +341,7 @@ void PortGroup::processPortIdList(PbRpcController *controller) this, SIGNAL(portGroupDataChanged(int, int))); qDebug("before port append\n"); mPorts.append(p); + atConnectPortConfig_.append(NULL); // will be filled later } emit portListChanged(mPortGroupId); @@ -365,8 +403,52 @@ void PortGroup::processPortConfigList(PbRpcController *controller) // design emit portListChanged(mPortGroupId); - if (numPorts() > 0) + if (numPorts() > 0) { + // XXX: The open session code (atConnectConfig_ related) assumes + // the following two RPCs are invoked in the below order + // Any change here without coressponding change in that code + // will break stuff + getDeviceGroupIdList(); getStreamIdList(); + } + + // Now that we have the port details, let's identify which ports + // need to be re-configured based on atConnectConfig_ + if (atConnectConfig_ && numPorts() > 0) + { + QString myself = appSettings->value(kUserKey, kUserDefaultValue) + .toString(); + for (int i = 0; i < atConnectConfig_->ports_size(); i++) + { + const OstProto::PortContent *pc = &atConnectConfig_->ports(i); + for (int j = 0; j < mPorts.size(); j++) + { + Port *port = mPorts[j]; + + if (port->name() == pc->port_config().name().c_str()) + { + if (!port->userName().isEmpty() // rsvd? + && port->userName() != myself) // by someone else? + { + QString warning = + QString("%1 - %2: %3 is reserved by %4.\n\n" + "Port will not be reconfigured.") + .arg(serverFullName()) + .arg(j) + .arg(port->userAlias()) + .arg(port->userName()); + QMessageBox::warning(NULL, tr("Open Session"), warning); + qWarning(qPrintable(warning)); + continue; + } + atConnectPortConfig_[j] = pc; + qDebug("port %d will be reconfigured", j); + break; + } + + } + } + } _error_exit: delete controller; @@ -387,6 +469,74 @@ void PortGroup::when_configApply(int portIndex) QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); mainWindow->setDisabled(true); + // FIXME: as currently written this code will make unnecessary RPCs + // even if the request contains no data; the fix will need to take + // care to identify when sync is complete + // NOTE: DeviceGroup RPCs are no longer called unnecessarily; + // Stream RPCs need to be fixed similarly + // Also, drone currently updates its packet list at the end of + // modifyStream() implicitly assuming that will be the last API + // called - this will also need to be fixed + + // + // Update/Sync DeviceGroups + // + OstProto::DeviceGroupIdList *deviceGroupIdList; + OstProto::DeviceGroupConfigList *deviceGroupConfigList; + bool refreshReqd = false; + + qDebug("applying 'deleted deviceGroups' ..."); + deviceGroupIdList = new OstProto::DeviceGroupIdList; + deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); + mPorts[portIndex]->getDeletedDeviceGroupsSinceLastSync(*deviceGroupIdList); + if (deviceGroupIdList->device_group_id_size()) { + ack = new OstProto::Ack; + controller = new PbRpcController(deviceGroupIdList, ack); + serviceStub->deleteDeviceGroup(controller, deviceGroupIdList, ack, + NewCallback(this, &PortGroup::processDeleteDeviceGroupAck, + controller)); + refreshReqd = true; + } + else + delete deviceGroupIdList; + + qDebug("applying 'new deviceGroups' ..."); + deviceGroupIdList = new OstProto::DeviceGroupIdList; + deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); + mPorts[portIndex]->getNewDeviceGroupsSinceLastSync(*deviceGroupIdList); + if (deviceGroupIdList->device_group_id_size()) { + ack = new OstProto::Ack; + controller = new PbRpcController(deviceGroupIdList, ack); + serviceStub->addDeviceGroup(controller, deviceGroupIdList, ack, + NewCallback(this, &PortGroup::processAddDeviceGroupAck, + controller)); + refreshReqd = true; + } + else + delete deviceGroupIdList; + + qDebug("applying 'modified deviceGroups' ..."); + deviceGroupConfigList = new OstProto::DeviceGroupConfigList; + deviceGroupConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id()); + mPorts[portIndex]->getModifiedDeviceGroupsSinceLastSync( + *deviceGroupConfigList); + if (deviceGroupConfigList->device_group_size()) { + ack = new OstProto::Ack; + controller = new PbRpcController(deviceGroupConfigList, ack); + serviceStub->modifyDeviceGroup(controller, deviceGroupConfigList, ack, + NewCallback(this, &PortGroup::processModifyDeviceGroupAck, + portIndex, controller)); + refreshReqd = true; + } + else + delete deviceGroupConfigList; + + if (refreshReqd) + getDeviceInfo(portIndex); + + // + // Update/Sync Streams + // qDebug("applying 'deleted streams' ..."); streamIdList = new OstProto::StreamIdList; ack = new OstProto::Ack; @@ -395,7 +545,7 @@ void PortGroup::when_configApply(int portIndex) streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getDeletedStreamsSinceLastSync(*streamIdList); - serviceStub->deleteStream(controller, streamIdList, ack, + serviceStub->deleteStream(controller, streamIdList, ack, NewCallback(this, &PortGroup::processDeleteStreamAck, controller)); qDebug("applying 'new streams' ..."); @@ -406,7 +556,7 @@ void PortGroup::when_configApply(int portIndex) streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getNewStreamsSinceLastSync(*streamIdList); - serviceStub->addStream(controller, streamIdList, ack, + serviceStub->addStream(controller, streamIdList, ack, NewCallback(this, &PortGroup::processAddStreamAck, controller)); qDebug("applying 'modified streams' ..."); @@ -417,9 +567,29 @@ void PortGroup::when_configApply(int portIndex) streamConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getModifiedStreamsSinceLastSync(*streamConfigList); - serviceStub->modifyStream(controller, streamConfigList, ack, - NewCallback(this, &PortGroup::processModifyStreamAck, + serviceStub->modifyStream(controller, streamConfigList, ack, + NewCallback(this, &PortGroup::processModifyStreamAck, portIndex, controller)); + +} + +void PortGroup::processAddDeviceGroupAck(PbRpcController *controller) +{ + qDebug("In %s", __FUNCTION__); + delete controller; +} + +void PortGroup::processDeleteDeviceGroupAck(PbRpcController *controller) +{ + qDebug("In %s", __FUNCTION__); + delete controller; +} + +void PortGroup::processModifyDeviceGroupAck(int portIndex, + PbRpcController *controller) +{ + qDebug("In %s", __FUNCTION__); + delete controller; } void PortGroup::processAddStreamAck(PbRpcController *controller) @@ -434,7 +604,7 @@ void PortGroup::processDeleteStreamAck(PbRpcController *controller) delete controller; } -void PortGroup::processModifyStreamAck(int portIndex, +void PortGroup::processModifyStreamAck(int portIndex, PbRpcController *controller) { qDebug("In %s", __FUNCTION__); @@ -444,7 +614,106 @@ void PortGroup::processModifyStreamAck(int portIndex, mainWindow->setEnabled(true); QApplication::restoreOverrideCursor(); + delete controller; +} +void PortGroup::getDeviceInfo(int portIndex) +{ + OstProto::PortId *portId; + OstProto::PortDeviceList *deviceList; + OstProto::PortNeighborList *neighList; + PbRpcController *controller; + + Q_ASSERT(portIndex < mPorts.size()); + + if (state() != QAbstractSocket::ConnectedState) + return; + + portId = new OstProto::PortId; + portId->set_id(mPorts[portIndex]->id()); + deviceList = new OstProto::PortDeviceList; + controller = new PbRpcController(portId, deviceList); + + serviceStub->getDeviceList(controller, portId, deviceList, + NewCallback(this, &PortGroup::processDeviceList, + portIndex, controller)); + + portId = new OstProto::PortId; + portId->set_id(mPorts[portIndex]->id()); + neighList = new OstProto::PortNeighborList; + controller = new PbRpcController(portId, neighList); + + serviceStub->getDeviceNeighbors(controller, portId, neighList, + NewCallback(this, &PortGroup::processDeviceNeighbors, + portIndex, controller)); +} + +void PortGroup::processDeviceList(int portIndex, PbRpcController *controller) +{ + OstProto::PortDeviceList *deviceList + = static_cast(controller->response()); + + qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); + + if (controller->Failed()) + { + qDebug("%s: rpc failed(%s)", __FUNCTION__, + qPrintable(controller->ErrorString())); + goto _exit; + } + + Q_ASSERT(portIndex < numPorts()); + + if (deviceList->port_id().id() != mPorts[portIndex]->id()) + { + qDebug("Invalid portId %d (expected %d) received for portIndex %d", + deviceList->port_id().id(), mPorts[portIndex]->id(), portIndex); + goto _exit; + } + + mPorts[portIndex]->clearDeviceList(); + for(int i = 0; i < deviceList->ExtensionSize(OstEmul::device); i++) { + mPorts[portIndex]->insertDevice( + deviceList->GetExtension(OstEmul::device, i)); + } + +_exit: + delete controller; +} + +void PortGroup::processDeviceNeighbors( + int portIndex, PbRpcController *controller) +{ + OstProto::PortNeighborList *neighList + = static_cast(controller->response()); + + qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); + + if (controller->Failed()) + { + qDebug("%s: rpc failed(%s)", __FUNCTION__, + qPrintable(controller->ErrorString())); + goto _exit; + } + + Q_ASSERT(portIndex < numPorts()); + + if (neighList->port_id().id() != mPorts[portIndex]->id()) + { + qDebug("Invalid portId %d (expected %d) received for portIndex %d", + neighList->port_id().id(), mPorts[portIndex]->id(), portIndex); + goto _exit; + } + + mPorts[portIndex]->clearDeviceNeighbors(); + for(int i=0; i < neighList->ExtensionSize(OstEmul::device_neighbor); i++) { + mPorts[portIndex]->insertDeviceNeighbors( + neighList->GetExtension(OstEmul::device_neighbor, i)); + } + + mPorts[portIndex]->deviceInfoRefreshed(); + +_exit: delete controller; } @@ -466,10 +735,10 @@ void PortGroup::modifyPort(int portIndex, OstProto::Port portConfig) PbRpcController *controller = new PbRpcController(portConfigList, ack); serviceStub->modifyPort(controller, portConfigList, ack, - NewCallback(this, &PortGroup::processModifyPortAck, controller)); + NewCallback(this, &PortGroup::processModifyPortAck, true, controller)); } -void PortGroup::processModifyPortAck(PbRpcController *controller) +void PortGroup::processModifyPortAck(bool restoreUi,PbRpcController *controller) { qDebug("In %s", __FUNCTION__); @@ -479,8 +748,10 @@ void PortGroup::processModifyPortAck(PbRpcController *controller) qPrintable(controller->ErrorString())); } - mainWindow->setEnabled(true); - QApplication::restoreOverrideCursor(); + if (restoreUi) { + mainWindow->setEnabled(true); + QApplication::restoreOverrideCursor(); + } delete controller; } @@ -534,6 +805,8 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller) { OstProto::StreamIdList *streamIdList = static_cast(controller->response()); + const OstProto::PortContent *newPortContent + = atConnectPortConfig_.at(portIndex); qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); @@ -553,50 +826,192 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller) goto _exit; } - for(int i = 0; i < streamIdList->stream_id_size(); i++) + if (newPortContent) { - uint streamId; + // This port needs to configured with new content - to do this + // we'll insert the following RPC sequence at this point and once + // this sequence is over, return to the regular RPC sequence by + // re-requesting getStreamId() + // * delete (existing) deviceGroups + // (already done by processDeviceIdList) + // * delete (existing) streams + // * modify port + // * add (new) deviceGroup ids + // * modify (new) deviceGroups + // * add (new) stream ids + // * modify (new) streams + // XXX: This assumes getDeviceGroupIdList() was invoked before + // getStreamIdList() - if the order changes this code will break! - streamId = streamIdList->stream_id(i).id(); - mPorts[portIndex]->insertStream(streamId); + // XXX: same name as input param, but shouldn't cause any problem + PbRpcController *controller; + quint32 portId = mPorts[portIndex]->id(); + QString myself = appSettings->value(kUserKey, kUserDefaultValue) + .toString(); + + // delete all existing streams + if (streamIdList->stream_id_size()) + { + OstProto::StreamIdList *streamIdList2 = new OstProto::StreamIdList; + streamIdList2->CopyFrom(*streamIdList); + + OstProto::Ack *ack = new OstProto::Ack; + controller = new PbRpcController(streamIdList2, ack); + + serviceStub->deleteStream(controller, streamIdList2, ack, + NewCallback(this, &PortGroup::processDeleteStreamAck, + controller)); + } + + OstProto::Port portCfg = newPortContent->port_config(); + if (mPorts[portIndex]->modifiablePortConfig(portCfg)) + { + OstProto::PortConfigList *portConfigList = + new OstProto::PortConfigList; + OstProto::Port *port = portConfigList->add_port(); + port->CopyFrom(portCfg); + if (port->has_user_name()) + port->set_user_name(qPrintable(myself)); // overwrite + + OstProto::Ack *ack = new OstProto::Ack; + controller = new PbRpcController(portConfigList, ack); + + serviceStub->modifyPort(controller, portConfigList, ack, + NewCallback(this, &PortGroup::processModifyPortAck, + false, controller)); + } + + // add/modify deviceGroups + if (newPortContent->device_groups_size()) + { + OstProto::DeviceGroupIdList *deviceGroupIdList + = new OstProto::DeviceGroupIdList; + OstProto::DeviceGroupConfigList *deviceGroupConfigList + = new OstProto::DeviceGroupConfigList; + deviceGroupIdList->mutable_port_id()->set_id(portId); + deviceGroupConfigList->mutable_port_id()->set_id(portId); + for (int i = 0; i < newPortContent->device_groups_size(); i++) + { + const OstProto::DeviceGroup &dg + = newPortContent->device_groups(i); + deviceGroupIdList->add_device_group_id()->set_id( + dg.device_group_id().id()); + deviceGroupConfigList->add_device_group()->CopyFrom(dg); + } + + OstProto::Ack *ack = new OstProto::Ack; + controller = new PbRpcController(deviceGroupIdList, ack); + + serviceStub->addDeviceGroup(controller, deviceGroupIdList, ack, + NewCallback(this, &PortGroup::processAddDeviceGroupAck, + controller)); + + ack = new OstProto::Ack; + controller = new PbRpcController(deviceGroupConfigList, ack); + + serviceStub->modifyDeviceGroup(controller, + deviceGroupConfigList, ack, + NewCallback(this, &PortGroup::processModifyDeviceGroupAck, + portIndex, controller)); + } + + // add/modify streams + if (newPortContent->streams_size()) + { + OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; + OstProto::StreamConfigList *streamConfigList = + new OstProto::StreamConfigList; + streamIdList->mutable_port_id()->set_id(portId); + streamConfigList->mutable_port_id()->set_id(portId); + for (int i = 0; i < newPortContent->streams_size(); i++) + { + const OstProto::Stream &s = newPortContent->streams(i); + streamIdList->add_stream_id()->set_id(s.stream_id().id()); + streamConfigList->add_stream()->CopyFrom(s); + } + + OstProto::Ack *ack = new OstProto::Ack; + controller = new PbRpcController(streamIdList, ack); + + serviceStub->addStream(controller, streamIdList, ack, + NewCallback(this, &PortGroup::processAddStreamAck, + controller)); + + ack = new OstProto::Ack; + controller = new PbRpcController(streamConfigList, ack); + + serviceStub->modifyStream(controller, streamConfigList, ack, + NewCallback(this, &PortGroup::processModifyStreamAck, + portIndex, controller)); + } + + // delete newPortConfig + atConnectPortConfig_[portIndex] = NULL; + + // return to normal sequence re-starting from + // getDeviceGroupIdList() and getStreamIdList() + OstProto::PortId *portId2 = new OstProto::PortId; + portId2->set_id(portId); + + OstProto::DeviceGroupIdList *devGrpIdList + = new OstProto::DeviceGroupIdList; + controller = new PbRpcController(portId2, devGrpIdList); + + serviceStub->getDeviceGroupIdList(controller, portId2, devGrpIdList, + NewCallback(this, &PortGroup::processDeviceGroupIdList, + portIndex, controller)); + + portId2 = new OstProto::PortId; + portId2->set_id(portId); + OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; + controller = new PbRpcController(portId2, streamIdList); + + serviceStub->getStreamIdList(controller, portId2, streamIdList, + NewCallback(this, &PortGroup::processStreamIdList, + portIndex, controller)); } - - mPorts[portIndex]->when_syncComplete(); - - // Are we done for all ports? - if (numPorts() && portIndex >= (numPorts()-1)) + else { - // FIXME(HI): some way to reset streammodel - getStreamConfigList(); + for(int i = 0; i < streamIdList->stream_id_size(); i++) + { + uint streamId; + + streamId = streamIdList->stream_id(i).id(); + mPorts[portIndex]->insertStream(streamId); + } + + mPorts[portIndex]->when_syncComplete(); + + getStreamConfigList(portIndex); } _exit: delete controller; } -void PortGroup::getStreamConfigList() +void PortGroup::getStreamConfigList(int portIndex) { - qDebug("requesting stream config list ..."); + if (mPorts[portIndex]->numStreams() == 0) + return; - for (int portIndex = 0; portIndex < numPorts(); portIndex++) + qDebug("requesting stream config list (port %d)...", portIndex); + + OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; + OstProto::StreamConfigList *streamConfigList + = new OstProto::StreamConfigList; + PbRpcController *controller = new PbRpcController( + streamIdList, streamConfigList); + + streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); + for (int j = 0; j < mPorts[portIndex]->numStreams(); j++) { - OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; - OstProto::StreamConfigList *streamConfigList - = new OstProto::StreamConfigList; - PbRpcController *controller = new PbRpcController( - streamIdList, streamConfigList); - - streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); - for (int j = 0; j < mPorts[portIndex]->numStreams(); j++) - { - OstProto::StreamId *s = streamIdList->add_stream_id(); - s->set_id(mPorts[portIndex]->streamByIndex(j)->id()); - } - - serviceStub->getStreamConfig(controller, streamIdList, streamConfigList, - NewCallback(this, &PortGroup::processStreamConfigList, - portIndex, controller)); + OstProto::StreamId *s = streamIdList->add_stream_id(); + s->set_id(mPorts[portIndex]->streamByIndex(j)->id()); } + + serviceStub->getStreamConfig(controller, streamIdList, streamConfigList, + NewCallback(this, &PortGroup::processStreamConfigList, + portIndex, controller)); } void PortGroup::processStreamConfigList(int portIndex, @@ -634,11 +1049,180 @@ void PortGroup::processStreamConfigList(int portIndex, streamConfigList->mutable_stream(i)); } +#if 0 + // FIXME: incorrect check - will never be true if last port does not have any streams configured // Are we done for all ports? - if (portIndex >= numPorts()) + if (portIndex >= (numPorts()-1)) { // FIXME(HI): some way to reset streammodel } +#endif + +_exit: + delete controller; +} + +void PortGroup::getDeviceGroupIdList() +{ + using OstProto::PortId; + using OstProto::DeviceGroupIdList; + + for (int portIndex = 0; portIndex < numPorts(); portIndex++) + { + PortId *portId = new PortId; + DeviceGroupIdList *devGrpIdList = new DeviceGroupIdList; + PbRpcController *controller = new PbRpcController(portId, devGrpIdList); + + portId->set_id(mPorts[portIndex]->id()); + + serviceStub->getDeviceGroupIdList(controller, portId, devGrpIdList, + NewCallback(this, &PortGroup::processDeviceGroupIdList, + portIndex, controller)); + } +} + +void PortGroup::processDeviceGroupIdList( + int portIndex, + PbRpcController *controller) +{ + using OstProto::DeviceGroupIdList; + + DeviceGroupIdList *devGrpIdList = static_cast( + controller->response()); + const OstProto::PortContent *newPortContent = atConnectPortConfig_.at( + portIndex); + + qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); + + if (controller->Failed()) + { + qDebug("%s: rpc failed(%s)", __FUNCTION__, + qPrintable(controller->ErrorString())); + goto _exit; + } + + Q_ASSERT(portIndex < numPorts()); + + if (devGrpIdList->port_id().id() != mPorts[portIndex]->id()) + { + qDebug("Invalid portId %d (expected %d) received for portIndex %d", + devGrpIdList->port_id().id(), mPorts[portIndex]->id(), portIndex); + goto _exit; + } + + if (newPortContent) + { + // We delete all existing deviceGroups + // Remaining stuff is done in processStreamIdList() - see notes there + if (devGrpIdList->device_group_id_size()) + { + OstProto::DeviceGroupIdList *devGrpIdList2 + = new OstProto::DeviceGroupIdList; + devGrpIdList2->CopyFrom(*devGrpIdList); + + OstProto::Ack *ack = new OstProto::Ack; + PbRpcController *controller + = new PbRpcController(devGrpIdList2, ack); + + serviceStub->deleteDeviceGroup(controller, devGrpIdList2, ack, + NewCallback(this, &PortGroup::processDeleteDeviceGroupAck, + controller)); + } + } + else + { + for(int i = 0; i < devGrpIdList->device_group_id_size(); i++) + { + uint devGrpId; + + devGrpId = devGrpIdList->device_group_id(i).id(); + mPorts[portIndex]->insertDeviceGroup(devGrpId); + } + + getDeviceGroupConfigList(portIndex); + } + +_exit: + delete controller; +} + +void PortGroup::getDeviceGroupConfigList(int portIndex) +{ + using OstProto::DeviceGroupId; + using OstProto::DeviceGroupIdList; + using OstProto::DeviceGroupConfigList; + + if (mPorts[portIndex]->numDeviceGroups() == 0) + return; + + qDebug("requesting device group config list (port %d) ...", portIndex); + + DeviceGroupIdList *devGrpIdList = new DeviceGroupIdList; + DeviceGroupConfigList *devGrpCfgList = new DeviceGroupConfigList; + PbRpcController *controller = new PbRpcController( + devGrpIdList, devGrpCfgList); + + devGrpIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); + for (int j = 0; j < mPorts[portIndex]->numDeviceGroups(); j++) + { + DeviceGroupId *dgid = devGrpIdList->add_device_group_id(); + dgid->set_id(mPorts[portIndex]->deviceGroupByIndex(j) + ->device_group_id().id()); + } + + serviceStub->getDeviceGroupConfig(controller, + devGrpIdList, devGrpCfgList, + NewCallback(this, &PortGroup::processDeviceGroupConfigList, + portIndex, controller)); +} + +void PortGroup::processDeviceGroupConfigList(int portIndex, + PbRpcController *controller) +{ + using OstProto::DeviceGroupConfigList; + + DeviceGroupConfigList *devGrpCfgList = + static_cast(controller->response()); + + qDebug("In %s", __PRETTY_FUNCTION__); + + Q_ASSERT(portIndex < numPorts()); + + if (controller->Failed()) + { + qDebug("%s: rpc failed(%s)", __FUNCTION__, + qPrintable(controller->ErrorString())); + goto _exit; + } + + Q_ASSERT(portIndex < numPorts()); + + if (devGrpCfgList->port_id().id() != mPorts[portIndex]->id()) + { + qDebug("Invalid portId %d (expected %d) received for portIndex %d", + devGrpCfgList->port_id().id(), mPorts[portIndex]->id(), portIndex); + goto _exit; + } + + for(int i = 0; i < devGrpCfgList->device_group_size(); i++) + { + uint dgid = devGrpCfgList->device_group(i).device_group_id().id(); + + mPorts[portIndex]->updateDeviceGroup(dgid, + devGrpCfgList->mutable_device_group(i)); + } + + if (devGrpCfgList->device_group_size()) + getDeviceInfo(portIndex); + +#if 0 + // FIXME: incorrect check - will never be true if last port does not have any deviceGroups configured + // Are we done for all ports? + if (portIndex >= (numPorts()-1)) + { + // FIXME: reset deviceGroupModel? + } +#endif _exit: delete controller; @@ -842,6 +1426,78 @@ _exit: delete controller; } +void PortGroup::resolveDeviceNeighbors(QList *portList) +{ + qDebug("In %s", __FUNCTION__); + + if (state() != QAbstractSocket::ConnectedState) + return; + + if ((portList == NULL) || (portList->size() == 0)) + goto _exit; + + { + OstProto::PortIdList *portIdList = new OstProto::PortIdList; + OstProto::Ack *ack = new OstProto::Ack; + PbRpcController *controller = new PbRpcController(portIdList, ack); + + for (int i = 0; i < portList->size(); i++) + { + OstProto::PortId *portId = portIdList->add_port_id(); + portId->set_id(portList->at(i)); + } + + serviceStub->resolveDeviceNeighbors(controller, portIdList, ack, + NewCallback(this, &PortGroup::processResolveDeviceNeighborsAck, + controller)); + } +_exit: + return; +} + +void PortGroup::processResolveDeviceNeighborsAck(PbRpcController *controller) +{ + qDebug("In %s", __FUNCTION__); + + delete controller; +} + +void PortGroup::clearDeviceNeighbors(QList *portList) +{ + qDebug("In %s", __FUNCTION__); + + if (state() != QAbstractSocket::ConnectedState) + return; + + if ((portList == NULL) || (portList->size() == 0)) + goto _exit; + + { + OstProto::PortIdList *portIdList = new OstProto::PortIdList; + OstProto::Ack *ack = new OstProto::Ack; + PbRpcController *controller = new PbRpcController(portIdList, ack); + + for (int i = 0; i < portList->size(); i++) + { + OstProto::PortId *portId = portIdList->add_port_id(); + portId->set_id(portList->at(i)); + } + + serviceStub->clearDeviceNeighbors(controller, portIdList, ack, + NewCallback(this, &PortGroup::processClearDeviceNeighborsAck, + controller)); + } +_exit: + return; +} + +void PortGroup::processClearDeviceNeighborsAck(PbRpcController *controller) +{ + qDebug("In %s", __FUNCTION__); + + delete controller; +} + void PortGroup::getPortStats() { //qDebug("In %s", __FUNCTION__); diff --git a/client/portgroup.h b/client/portgroup.h index 7accbcf..ef60f09 100644 --- a/client/portgroup.h +++ b/client/portgroup.h @@ -36,6 +36,11 @@ LOW #define DEFAULT_SERVER_PORT 7878 +namespace OstProto { + class PortContent; + class PortGroupContent; +} + class QFile; class QTimer; @@ -62,6 +67,9 @@ private: OstProto::PortIdList *portIdList_; OstProto::PortStatsList *portStatsList_; + OstProto::PortGroupContent *atConnectConfig_; + QList atConnectPortConfig_; + public: // FIXME(HIGH): member access QList mPorts; @@ -82,7 +90,10 @@ public: } void disconnectFromHost() { reconnect = false; rpcChannel->tearDown(); } + void setConfigAtConnect(const OstProto::PortGroupContent *config); + int numPorts() const { return mPorts.size(); } + int numReservedPorts() const; quint32 id() const { return mPortGroupId; } const QString& userAlias() const { return mUserAlias; } @@ -92,6 +103,7 @@ public: { return rpcChannel->serverName(); } quint16 serverPort() const { return rpcChannel->serverPort(); } + const QString serverFullName() const; QAbstractSocket::SocketState state() const { if (compat == kIncompatible) return QAbstractSocket::SocketState(-1); @@ -106,17 +118,31 @@ public: void processDeleteStreamAck(PbRpcController *controller); void processModifyStreamAck(int portIndex, PbRpcController *controller); + void processAddDeviceGroupAck(PbRpcController *controller); + void processDeleteDeviceGroupAck(PbRpcController *controller); + void processModifyDeviceGroupAck(int portIndex, PbRpcController *controller); + + void processDeviceList(int portIndex, PbRpcController *controller); + void processDeviceNeighbors(int portIndex, PbRpcController *controller); + void modifyPort(int portId, OstProto::Port portConfig); - void processModifyPortAck(PbRpcController *controller); + void processModifyPortAck(bool restoreUi, PbRpcController *controller); void processUpdatedPortConfig(PbRpcController *controller); void getStreamIdList(); void processStreamIdList(int portIndex, PbRpcController *controller); - void getStreamConfigList(); + void getStreamConfigList(int portIndex); void processStreamConfigList(int portIndex, PbRpcController *controller); void processModifyStreamAck(OstProto::Ack *ack); + void getDeviceGroupIdList(); + void processDeviceGroupIdList(int portIndex, PbRpcController *controller); + void getDeviceGroupConfigList(int portIndex); + void processDeviceGroupConfigList( + int portIndex, + PbRpcController *controller); + void startTx(QList *portList = NULL); void processStartTxAck(PbRpcController *controller); void stopTx(QList *portList = NULL); @@ -129,6 +155,11 @@ public: void viewCapture(QList *portList = NULL); void processViewCaptureAck(PbRpcController *controller); + void resolveDeviceNeighbors(QList *portList = NULL); + void processResolveDeviceNeighborsAck(PbRpcController *controller); + void clearDeviceNeighbors(QList *portList = NULL); + void processClearDeviceNeighborsAck(PbRpcController *controller); + void getPortStats(); void processPortStatsList(); void clearPortStats(QList *portList = NULL); @@ -154,6 +185,7 @@ private slots: public slots: void when_configApply(int portIndex); + void getDeviceInfo(int portIndex); }; diff --git a/client/portgrouplist.cpp b/client/portgrouplist.cpp index cfdc74b..6e380a0 100644 --- a/client/portgrouplist.cpp +++ b/client/portgrouplist.cpp @@ -25,7 +25,9 @@ along with this program. If not, see PortGroupList::PortGroupList() : mPortGroupListModel(this), mStreamListModel(this), - mPortStatsModel(this, this) + mPortStatsModel(this, this), + mDeviceGroupModel(this), + mDeviceModel(this) { PortGroup *pg; @@ -33,10 +35,14 @@ PortGroupList::PortGroupList() streamModelTester_ = NULL; portModelTester_ = NULL; portStatsModelTester_ = NULL; + deviceGroupModelTester_ = NULL; + deviceModelTester_ = NULL; #else streamModelTester_ = new ModelTest(getStreamModel()); portModelTester_ = new ModelTest(getPortModel()); portStatsModelTester_ = new ModelTest(getPortStatsModel()); + deviceGroupModelTester_ = new ModelTest(getDeviceGroupModel()); + deviceModelTester_ = new ModelTest(getDeviceModel()); #endif // Add the "Local" Port Group @@ -49,10 +55,10 @@ PortGroupList::~PortGroupList() delete portStatsModelTester_; delete portModelTester_; delete streamModelTester_; + delete deviceGroupModelTester_; while (!mPortGroups.isEmpty()) delete mPortGroups.takeFirst(); - } bool PortGroupList::isPortGroup(const QModelIndex& index) @@ -120,6 +126,23 @@ void PortGroupList::removePortGroup(PortGroup &portGroup) mPortStatsModel.when_portListChanged(); } +void PortGroupList::removeAllPortGroups() +{ + if (mPortGroups.isEmpty()) + return; + + do { + PortGroup *pg = mPortGroups.at(0); + mPortGroupListModel.portGroupAboutToBeRemoved(pg); + mPortGroups.removeFirst(); + delete pg; + } while (!mPortGroups.isEmpty()); + mPortGroupListModel.portGroupRemoved(); + + mPortGroupListModel.when_portListChanged(); + mPortStatsModel.when_portListChanged(); +} + //.................... // Private Methods //.................... diff --git a/client/portgrouplist.h b/client/portgrouplist.h index 3083c26..22f8c84 100644 --- a/client/portgrouplist.h +++ b/client/portgrouplist.h @@ -20,12 +20,12 @@ along with this program. If not, see #ifndef _PORT_GROUP_LIST_H #define _PORT_GROUP_LIST_H +#include "devicegroupmodel.h" +#include "devicemodel.h" #include "portgroup.h" -#include -#include #include "portmodel.h" -#include "streammodel.h" #include "portstatsmodel.h" +#include "streammodel.h" class PortModel; class StreamModel; @@ -42,10 +42,14 @@ class PortGroupList : public QObject { PortModel mPortGroupListModel; StreamModel mStreamListModel; PortStatsModel mPortStatsModel; + DeviceGroupModel mDeviceGroupModel; + DeviceModel mDeviceModel; QObject *streamModelTester_; QObject *portModelTester_; QObject *portStatsModelTester_; + QObject *deviceGroupModelTester_; + QObject *deviceModelTester_; // Methods public: @@ -55,6 +59,8 @@ public: PortModel* getPortModel() { return &mPortGroupListModel; } PortStatsModel* getPortStatsModel() { return &mPortStatsModel; } StreamModel* getStreamModel() { return &mStreamListModel; } + DeviceGroupModel* getDeviceGroupModel() { return &mDeviceGroupModel; } + DeviceModel* getDeviceModel() { return &mDeviceModel; } bool isPortGroup(const QModelIndex& index); bool isPort(const QModelIndex& index); @@ -66,6 +72,7 @@ public: void addPortGroup(PortGroup &portGroup); void removePortGroup(PortGroup &portGroup); + void removeAllPortGroups(); private: int indexOfPortGroup(quint32 portGroupId); diff --git a/client/portmodel.cpp b/client/portmodel.cpp index 479e2bc..cd1c9d7 100644 --- a/client/portmodel.cpp +++ b/client/portmodel.cpp @@ -179,7 +179,7 @@ QVariant PortModel::data(const QModelIndex &index, int role) const return QString("Port %1: %2 %3(%4)") .arg(port->id()) - .arg(port->name()) + .arg(port->userAlias()) .arg(rsvdBy) .arg(port->description()); } diff --git a/client/portmodel.h b/client/portmodel.h index 2027f0b..4464fdf 100644 --- a/client/portmodel.h +++ b/client/portmodel.h @@ -57,6 +57,7 @@ private: QIcon portIconFactory[kLinkStatesCount][kExclusiveStatesCount]; private slots: + // FIXME: these are invoked from outside - how come they are "private"? void when_portGroupDataChanged(int portGroupId, int portId); void portGroupAboutToBeAppended(); diff --git a/client/portstatswindow.cpp b/client/portstatswindow.cpp index 3e2cb9a..0e7b73f 100644 --- a/client/portstatswindow.cpp +++ b/client/portstatswindow.cpp @@ -148,6 +148,36 @@ void PortStatsWindow::on_tbViewCapture_clicked() } } +void PortStatsWindow::on_tbResolveNeighbors_clicked() +{ + QList portList; + + // Get selected ports + model->portListFromIndex(selectedColumns(), portList); + + // Resolve ARP/ND for selected ports, portgroup by portgroup + for (int i = 0; i < portList.size(); i++) + { + pgl->portGroupByIndex(portList.at(i).portGroupId). + resolveDeviceNeighbors(&portList[i].portList); + } +} + +void PortStatsWindow::on_tbClearNeighbors_clicked() +{ + QList portList; + + // Get selected ports + model->portListFromIndex(selectedColumns(), portList); + + // Clear ARP/ND for ports, portgroup by portgroup + for (int i = 0; i < portList.size(); i++) + { + pgl->portGroupByIndex(portList.at(i).portGroupId). + clearDeviceNeighbors(&portList[i].portList); + } +} + void PortStatsWindow::on_tbClear_clicked() { QList portList; diff --git a/client/portstatswindow.h b/client/portstatswindow.h index 39a2efb..dfb5fbf 100644 --- a/client/portstatswindow.h +++ b/client/portstatswindow.h @@ -50,6 +50,9 @@ private slots: void on_tbClear_clicked(); void on_tbClearAll_clicked(); + void on_tbResolveNeighbors_clicked(); + void on_tbClearNeighbors_clicked(); + void on_tbFilter_clicked(); private: diff --git a/client/portstatswindow.ui b/client/portstatswindow.ui index 8c6aed4..870633e 100644 --- a/client/portstatswindow.ui +++ b/client/portstatswindow.ui @@ -141,6 +141,45 @@ + + + + Resolve Neighbors + + + Resolve Device Neighbors on selected port(s) + + + Resolve Neighbors + + + :/icons/neighbor_resolve.png + + + + + + + Clear Neighbors + + + Clear Device Neighbors on selected port(s) + + + Clear Neighbors + + + :/icons/neighbor_clear.png + + + + + + + Qt::Vertical + + + diff --git a/client/portswindow.cpp b/client/portswindow.cpp index 21668f2..4c85150 100644 --- a/client/portswindow.cpp +++ b/client/portswindow.cpp @@ -19,18 +19,24 @@ along with this program. If not, see #include "portswindow.h" -#include "abstractfileformat.h" +#include "deviceswidget.h" #include "portconfigdialog.h" #include "settings.h" #include "streamconfigdialog.h" +#include "streamfileformat.h" #include "streamlistdelegate.h" +#include "fileformat.pb.h" + #include #include #include +#include #include #include +extern QMainWindow *mainWindow; + PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) : QWidget(parent), proxyPortModel(NULL) { @@ -44,6 +50,7 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) plm = pgl; setupUi(this); + devicesWidget->setPortGroupList(plm); tvPortList->header()->hide(); @@ -61,7 +68,7 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) tvPortList->addAction(actionExclusive_Control); tvPortList->addAction(actionPort_Configuration); - // Populate StramList Context Menu Actions + // Populate StreamList Context Menu Actions tvStreamList->addAction(actionNew_Stream); tvStreamList->addAction(actionEdit_Stream); tvStreamList->addAction(actionDuplicate_Stream); @@ -74,12 +81,17 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) tvStreamList->addAction(actionOpen_Streams); tvStreamList->addAction(actionSave_Streams); - // PortList and StreamList actions combined make this window's actions + // PortList, StreamList, DeviceWidget actions combined + // make this window's actions addActions(tvPortList->actions()); sep = new QAction(this); sep->setSeparator(true); addAction(sep); addActions(tvStreamList->actions()); + sep = new QAction(this); + sep->setSeparator(true); + addAction(sep); + addActions(devicesWidget->actions()); tvStreamList->setModel(plm->getStreamModel()); @@ -112,6 +124,9 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(when_portView_currentChanged(const QModelIndex&, const QModelIndex&))); + connect(this, + SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)), + devicesWidget, SLOT(setCurrentPortIndex(const QModelIndex&))); connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(updateStreamViewActions())); @@ -128,7 +143,8 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) tvStreamList->resizeColumnToContents(StreamModel::StreamIcon); tvStreamList->resizeColumnToContents(StreamModel::StreamStatus); - // Initially we don't have any ports/streams - so send signal triggers + // Initially we don't have any ports/streams/devices + // - so send signal triggers when_portView_currentChanged(QModelIndex(), QModelIndex()); updateStreamViewActions(); @@ -157,6 +173,116 @@ PortsWindow::~PortsWindow() delete proxyPortModel; } +int PortsWindow::portGroupCount() +{ + return plm->numPortGroups(); +} + +int PortsWindow::reservedPortCount() +{ + int count = 0; + int n = portGroupCount(); + + for (int i = 0; i < n; i++) + count += plm->portGroupByIndex(i).numReservedPorts(); + + return count; +} + +//! Always return true +bool PortsWindow::openSession( + const OstProto::SessionContent *session, + QString &error) +{ + QProgressDialog progress("Opening Session", NULL, + 0, session->port_groups_size(), mainWindow); + progress.show(); + progress.setEnabled(true); // since parent (mainWindow) is disabled + + plm->removeAllPortGroups(); + + for (int i = 0; i < session->port_groups_size(); i++) { + const OstProto::PortGroupContent &pgc = session->port_groups(i); + PortGroup *pg = new PortGroup(QString::fromStdString( + pgc.server_name()), + quint16(pgc.server_port())); + pg->setConfigAtConnect(&pgc); + plm->addPortGroup(*pg); + progress.setValue(i+1); + } + + return true; +} + +/*! + * Prepare content to be saved for a session + * + * If port reservation is in use, saves only 'my' reserved ports + * + * Returns false, if user cancels op; true, otherwise + */ +bool PortsWindow::saveSession( + OstProto::SessionContent *session, // OUT param + QString &error, + QProgressDialog *progress) +{ + int n = portGroupCount(); + QString myself; + + if (progress) { + progress->setLabelText("Preparing Ports and PortGroups ..."); + progress->setRange(0, n); + } + + if (reservedPortCount()) + myself = appSettings->value(kUserKey, kUserDefaultValue).toString(); + + for (int i = 0; i < n; i++) + { + PortGroup &pg = plm->portGroupByIndex(i); + OstProto::PortGroupContent *pgc = session->add_port_groups(); + + pgc->set_server_name(pg.serverName().toStdString()); + pgc->set_server_port(pg.serverPort()); + + for (int j = 0; j < pg.numPorts(); j++) + { + if (myself != pg.mPorts.at(j)->userName()) + continue; + + OstProto::PortContent *pc = pgc->add_ports(); + OstProto::Port *p = pc->mutable_port_config(); + + // XXX: We save the entire OstProto::Port even though some + // fields may be ephemeral; while opening we use only relevant + // fields + pg.mPorts.at(j)->protoDataCopyInto(p); + + for (int k = 0; k < pg.mPorts.at(j)->numStreams(); k++) + { + OstProto::Stream *s = pc->add_streams(); + pg.mPorts.at(j)->streamByIndex(k)->protoDataCopyInto(*s); + } + + for (int k = 0; k < pg.mPorts.at(j)->numDeviceGroups(); k++) + { + OstProto::DeviceGroup *dg = pc->add_device_groups(); + dg->CopyFrom(*(pg.mPorts.at(j)->deviceGroupByIndex(k))); + } + } + + if (progress) { + if (progress->wasCanceled()) + return false; + progress->setValue(i); + } + if (i % 2 == 0) + qApp->processEvents(); + } + + return true; +} + void PortsWindow::showMyReservedPortsOnly(bool enabled) { if (!proxyPortModel) @@ -240,6 +366,8 @@ void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex, SLOT(updatePortRates())); } } + + emit currentPortChanged(current, previous); } void PortsWindow::when_portModel_dataChanged(const QModelIndex& topLeft, @@ -684,6 +812,9 @@ void PortsWindow::on_actionOpen_Streams_triggered() { qDebug("Open Streams Action"); + QStringList fileTypes = StreamFileFormat::supportedFileTypes( + StreamFileFormat::kOpenFile); + QString fileType; QModelIndex current = tvPortList->selectionModel()->currentIndex(); static QString dirName; QString fileName; @@ -696,7 +827,11 @@ void PortsWindow::on_actionOpen_Streams_triggered() Q_ASSERT(plm->isPort(current)); - fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"), dirName); + if (fileTypes.size()) + fileType = fileTypes.at(0); + + fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"), + dirName, fileTypes.join(";;"), &fileType); if (fileName.isEmpty()) goto _exit; @@ -750,7 +885,8 @@ void PortsWindow::on_actionSave_Streams_triggered() QModelIndex current = tvPortList->selectionModel()->currentIndex(); static QString fileName; - QStringList fileTypes = AbstractFileFormat::supportedFileTypes(); + QStringList fileTypes = StreamFileFormat::supportedFileTypes( + StreamFileFormat::kSaveFile); QString fileType; QString errorStr; QFileDialog::Options options; diff --git a/client/portswindow.h b/client/portswindow.h index 99f81e9..b407270 100644 --- a/client/portswindow.h +++ b/client/portswindow.h @@ -25,15 +25,14 @@ along with this program. If not, see #include "ui_portswindow.h" #include "portgrouplist.h" -/* TODO -HIGH -MED -LOW -*/ - class QAbstractItemDelegate; +class QProgressDialog; class QSortFilterProxyModel; +namespace OstProto { + class SessionContent; +} + class PortsWindow : public QWidget, private Ui::PortsWindow { Q_OBJECT @@ -45,6 +44,19 @@ public: PortsWindow(PortGroupList *pgl, QWidget *parent = 0); ~PortsWindow(); + int portGroupCount(); + int reservedPortCount(); + + bool openSession(const OstProto::SessionContent *session, + QString &error); + bool saveSession(OstProto::SessionContent *session, + QString &error, + QProgressDialog *progress = NULL); + +signals: + void currentPortChanged(const QModelIndex ¤t, + const QModelIndex &previous); + private: QString lastNewPortGroup; QAbstractItemDelegate *delegate; diff --git a/client/portswindow.ui b/client/portswindow.ui index d3a2c55..cbab508 100644 --- a/client/portswindow.ui +++ b/client/portswindow.ui @@ -5,7 +5,7 @@ 0 0 - 710 + 663 352 @@ -22,6 +22,12 @@ false + + + 1 + 0 + + Qt::ActionsContextMenu @@ -30,11 +36,17 @@ + + + 2 + 0 + + 0 - + 0 @@ -47,115 +59,141 @@ 0 - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - - - Avg pps - - - true - - - - - - - - - - Avg bps - - - - - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - + + + + QFrame::Panel + + + QFrame::Raised + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply + + + + + + + + + + 0 + + + + Streams + - - - Apply + + + + + Avg pps + + + true + + + + + + + + + + Avg bps + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 1 + + + + Qt::ActionsContextMenu + + + QFrame::StyledPanel + + + 1 + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + Devices + + - - - Qt::Vertical - - - - 20 - 0 - - - + - - - - - - - - 0 - 1 - - - - Qt::ActionsContextMenu - - - QFrame::StyledPanel - - - 1 - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - + @@ -267,6 +305,14 @@ + + + DevicesWidget + QWidget +
deviceswidget.h
+ 1 +
+
@@ -278,12 +324,12 @@ setEnabled(bool) - 313 - 28 + 326 + 80 - 380 - 28 + 454 + 79 @@ -294,12 +340,12 @@ setEnabled(bool) - 333 - 55 + 523 + 80 - 395 - 56 + 651 + 88 diff --git a/client/stream.cpp b/client/stream.cpp index 2305930..6bc9ab5 100644 --- a/client/stream.cpp +++ b/client/stream.cpp @@ -49,3 +49,13 @@ void Stream::storeProtocolWidgets() qWarning("%s: DOES NOTHING", __PRETTY_FUNCTION__); return; } + +quint64 getDeviceMacAddress(int portId, int streamId, int frameIndex) +{ + return 0; +} + +quint64 getNeighborMacAddress(int portId, int streamId, int frameIndex) +{ + return 0; +} diff --git a/common/emulation.h b/common/emulation.h new file mode 100644 index 0000000..919849d --- /dev/null +++ b/common/emulation.h @@ -0,0 +1,55 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _EMULATION_H +#define _EMULATION_H + +#include "emulproto.pb.h" + +#include + +static inline OstProto::DeviceGroup* newDeviceGroup(uint portId) +{ + OstProto::DeviceGroup *devGrp = new OstProto::DeviceGroup; + + // To ensure that DeviceGroup have a unique key, we assign + // a random mac address upon creation; ideally, it would + // have been good to inherit OstProto::DeviceGroup and define + // a new constructor, but ProtoBuf forbids against inheriting + // generated classes, so we use this helper function instead + // + // Create a mac address as per RFC 4814 Sec 4.2 + // (RR & 0xFC):PP:PP:RR:RR:RR + // where RR is a random number, PP:PP is 1-indexed port index + // NOTE: although qrand() return type is a int, the max value + // is RAND_MAX (stdlib.h) which is often 16-bit only, so we + // use two random numbers + quint32 r1 = qrand(), r2 = qrand(); + quint64 mac; + mac = quint64(r1 & 0xfc00) << 32 + | quint64(portId + 1) << 24 + | quint64((r1 & 0xff) << 16 | (r2 & 0xffff)); + devGrp->MutableExtension(OstEmul::mac)->set_address(mac); + + return devGrp; +} + + +#endif + diff --git a/common/emulproto.proto b/common/emulproto.proto new file mode 100644 index 0000000..8c34d9b --- /dev/null +++ b/common/emulproto.proto @@ -0,0 +1,122 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +import "protocol.proto"; + +package OstEmul; + +// ======= +// Encap +// ======= +message VlanEmulation { + message Vlan { + optional uint32 tpid = 1 [default = 0x8100]; + + // includes prio, cfi and vlanid + optional uint32 vlan_tag = 2 [default = 100]; + + optional uint32 count = 10 [default = 1]; + optional uint32 step = 11 [default = 1]; + } + + repeated Vlan stack = 1; // outer to inner +} + +extend OstProto.EncapEmulation { + optional VlanEmulation vlan = 1000; +} + +// =========== +// Protocols +// =========== +message MacEmulation { + optional uint64 address = 1; // no default - need unique value + optional uint64 step = 10 [default = 1]; +} + +// No default values for IP addresses - user needs to explicitly set that +// 'coz we derive if a device has a single/dual or no IP stack on the basis +// of whether OstProto.DeviceGroup.ip[46] is set +message Ip4Emulation { + optional uint32 address = 1; + optional uint32 prefix_length = 2 [default = 24]; + optional uint32 default_gateway = 3; + + optional uint32 step = 10 [default = 1]; + // FIXME: step for gateway? +} + +message Ip6Address { + optional uint64 hi = 1; + optional uint64 lo = 2; +} + +message Ip6Emulation { + optional Ip6Address address = 1; + optional uint32 prefix_length = 2 [default = 64]; + optional Ip6Address default_gateway = 3; + + optional Ip6Address step = 10; + // FIXME: step for gateway? +} + +extend OstProto.DeviceGroup { + optional MacEmulation mac = 2001; + + optional Ip4Emulation ip4 = 3000; + optional Ip6Emulation ip6 = 3001; +} + +message Device { + optional uint64 mac = 1; + + repeated uint32 vlan = 2; // includes tpid 'n vlan tag + + optional uint32 ip4 = 10; + optional uint32 ip4_prefix_length = 11; + optional uint32 ip4_default_gateway = 12; + + optional Ip6Address ip6 = 20; + optional uint32 ip6_prefix_length = 21; + optional Ip6Address ip6_default_gateway = 22; +} + +extend OstProto.PortDeviceList { + repeated Device device = 100; +} + +message ArpEntry { + optional uint32 ip4 = 1; + optional uint64 mac = 2; +} + +message NdpEntry { + optional Ip6Address ip6 = 1; + optional uint64 mac = 2; +} + +message DeviceNeighborList { + optional uint32 device_index = 1; + repeated ArpEntry arp = 2; + repeated NdpEntry ndp = 3; +} + +extend OstProto.PortNeighborList { + repeated DeviceNeighborList device_neighbor = 100; +} diff --git a/common/fileformat.proto b/common/fileformat.proto index ce2a688..10339d7 100644 --- a/common/fileformat.proto +++ b/common/fileformat.proto @@ -24,6 +24,7 @@ package OstProto; enum FileType { kReservedFileType = 0; kStreamsFileType = 1; + kSessionFileType = 10; } message FileMetaData { @@ -36,8 +37,30 @@ message FileMetaData { required string generator_revision = 7; } +message PortContent { + optional Port port_config = 1; + repeated Stream streams = 2; + repeated DeviceGroup device_groups = 3; +} + +message PortGroupContent { + optional string server_name = 1; + optional uint32 server_port = 2; + + repeated PortContent ports = 15; +} + +message SessionContent { + repeated PortGroupContent port_groups = 1; +} + message FileContentMatter { optional StreamConfigList streams = 1; + // TODO: optional DeviceGroupConfigList device_groups = 2; + // TODO: optional PortContent port = 3; + // FIXME: (single) portgroup? is there a usecase for this? + + optional SessionContent session = 10; } /* diff --git a/common/intedit.h b/common/intedit.h new file mode 100644 index 0000000..e5392e8 --- /dev/null +++ b/common/intedit.h @@ -0,0 +1,41 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _INT_EDIT_H +#define _INT_EDIT_H + +#include + +#include + +class IntEdit: public QSpinBox +{ +public: + IntEdit(QWidget *parent = 0); +}; + +inline IntEdit::IntEdit(QWidget *parent) + : QSpinBox(parent) +{ + setRange(INT_MIN, INT_MAX); + setButtonSymbols(QAbstractSpinBox::NoButtons); +} + +#endif + diff --git a/common/ip4edit.h b/common/ip4edit.h new file mode 100644 index 0000000..1efc717 --- /dev/null +++ b/common/ip4edit.h @@ -0,0 +1,52 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _IP4_EDIT_H +#define _IP4_EDIT_H + +#include +#include + +class Ip4Edit: public QLineEdit +{ +public: + Ip4Edit(QWidget *parent = 0); + + quint32 value(); + void setValue(quint32 val); +}; + +inline Ip4Edit::Ip4Edit(QWidget *parent) + : QLineEdit(parent) +{ + setInputMask(QString("000.000.000.000; ")); +} + +inline quint32 Ip4Edit::value() +{ + return QHostAddress(text()).toIPv4Address(); +} + +inline void Ip4Edit::setValue(quint32 val) +{ + setText(QHostAddress(val).toString()); +} + +#endif + diff --git a/common/ip6edit.h b/common/ip6edit.h new file mode 100644 index 0000000..891461b --- /dev/null +++ b/common/ip6edit.h @@ -0,0 +1,79 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _IP6_EDIT_H +#define _IP6_EDIT_H + +#include "ipv6addressvalidator.h" +#include "uint128.h" + +#include +#include + +class Ip6Edit: public QLineEdit +{ +public: + Ip6Edit(QWidget *parent = 0); + + UInt128 value(); + quint64 valueHi64(); + quint64 valueLo64(); + void setValue(UInt128 val); + void setValue(quint64 hi, quint64 lo); + void setValue(const QString &val); +}; + +inline Ip6Edit::Ip6Edit(QWidget *parent) + : QLineEdit(parent) +{ + setValidator(new IPv6AddressValidator(this)); +} + +inline UInt128 Ip6Edit::value() +{ + Q_IPV6ADDR addr = QHostAddress(text()).toIPv6Address(); + return UInt128((quint8*)&addr); +} + +inline quint64 Ip6Edit::valueHi64() +{ + return value().hi64(); +} + +inline quint64 Ip6Edit::valueLo64() +{ + return value().lo64(); +} + +inline void Ip6Edit::setValue(UInt128 val) +{ + setText(QHostAddress(val.toArray()).toString()); +} + +inline void Ip6Edit::setValue(quint64 hi, quint64 lo) +{ + UInt128 ip(hi, lo); + setValue(ip); +} + +inline void Ip6Edit::setValue(const QString &val) +{ + setText(QHostAddress(val).toString()); +} +#endif diff --git a/common/mac.cpp b/common/mac.cpp index cfe35a6..3a7df21 100644 --- a/common/mac.cpp +++ b/common/mac.cpp @@ -19,6 +19,8 @@ along with this program. If not, see #include "mac.h" +#include "../common/streambase.h" + #include #define uintToMacStr(num) \ @@ -28,6 +30,7 @@ along with this program. If not, see MacProtocol::MacProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { + forResolve_ = false; } MacProtocol::~MacProtocol() @@ -124,6 +127,15 @@ QVariant MacProtocol::fieldData(int index, FieldAttrib attrib, data.dst_mac_step(); dstMac = data.dst_mac() - u; break; + case OstProto::Mac::e_mm_resolve: + if (forResolve_) + dstMac = 0; + else { + forResolve_ = true; + dstMac = mpStream->neighborMacAddress(streamIndex); + forResolve_ = false; + } + break; default: qWarning("Unhandled dstMac_mode %d", data.dst_mac_mode()); } @@ -169,6 +181,15 @@ QVariant MacProtocol::fieldData(int index, FieldAttrib attrib, data.src_mac_step(); srcMac = data.src_mac() - u; break; + case OstProto::Mac::e_mm_resolve: + if (forResolve_) + srcMac = 0; + else { + forResolve_ = true; + srcMac = mpStream->deviceMacAddress(streamIndex); + forResolve_ = false; + } + break; default: qWarning("Unhandled srcMac_mode %d", data.src_mac_mode()); } @@ -331,11 +352,23 @@ int MacProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); - if (data.dst_mac_mode() != OstProto::Mac::e_mm_fixed) - count = AbstractProtocol::lcm(count, data.dst_mac_count()); + switch (data.dst_mac_mode()) { + case OstProto::Mac::e_mm_inc: + case OstProto::Mac::e_mm_dec: + count = AbstractProtocol::lcm(count, data.dst_mac_count()); + break; + default: + break; + } - if (data.src_mac_mode() != OstProto::Mac::e_mm_fixed) - count = AbstractProtocol::lcm(count, data.src_mac_count()); + switch (data.src_mac_mode()) { + case OstProto::Mac::e_mm_inc: + case OstProto::Mac::e_mm_dec: + count = AbstractProtocol::lcm(count, data.src_mac_count()); + break; + default: + break; + } return count; } diff --git a/common/mac.h b/common/mac.h index 3469d40..792f325 100644 --- a/common/mac.h +++ b/common/mac.h @@ -67,6 +67,7 @@ public: private: OstProto::Mac data; + mutable bool forResolve_; }; #endif diff --git a/common/mac.proto b/common/mac.proto index 2055223..1bd07f4 100644 --- a/common/mac.proto +++ b/common/mac.proto @@ -28,6 +28,7 @@ message Mac { e_mm_fixed = 0; e_mm_inc = 1; e_mm_dec = 2; + e_mm_resolve = 3; // dst: resolve neighbor; src: from device config } // Dst Mac diff --git a/common/mac.ui b/common/mac.ui index 821cf00..f3604bb 100644 --- a/common/mac.ui +++ b/common/mac.ui @@ -5,8 +5,8 @@ 0 0 - 391 - 116 + 400 + 200 @@ -81,6 +81,11 @@ Decrement + + + Resolve + + @@ -143,6 +148,11 @@ Decrement + + + Resolve + + @@ -168,7 +178,17 @@ - + + + + Please ensure that a corresponding device is configured on the port to enable source/destination mac address resolution. A corresponding device is one which has VLANs and source/gateway IP corresponding to this stream. + + + true + + + + Qt::Vertical diff --git a/common/macconfig.cpp b/common/macconfig.cpp index f17c140..1e50854 100644 --- a/common/macconfig.cpp +++ b/common/macconfig.cpp @@ -28,6 +28,12 @@ MacConfigForm::MacConfigForm(QWidget *parent) QRegExp reMac("([0-9,a-f,A-F]{2,2}[:-]){5,5}[0-9,a-f,A-F]{2,2}"); setupUi(this); + resolveInfo->hide(); +#if 0 + // not working for some reason + resolveInfo->setPixmap(resolveInfo->style()->standardIcon( + QStyle::SP_MessageBoxInformation).pixmap(128)); +#endif leDstMac->setValidator(new QRegExpValidator(reMac, this)); leSrcMac->setValidator(new QRegExpValidator(reMac, this)); leDstMacCount->setValidator(new QIntValidator(1, MAX_MAC_ITER_COUNT, this)); @@ -46,30 +52,50 @@ MacConfigForm* MacConfigForm::createInstance() void MacConfigForm::on_cmbDstMacMode_currentIndexChanged(int index) { - if (index == OstProto::Mac::e_mm_fixed) - { - leDstMacCount->setEnabled(false); - leDstMacStep->setEnabled(false); - } - else - { - leDstMacCount->setEnabled(true); - leDstMacStep->setEnabled(true); + switch (index) { + case OstProto::Mac::e_mm_resolve: + leDstMac->setEnabled(false); + leDstMacCount->setEnabled(false); + leDstMacStep->setEnabled(false); + break; + case OstProto::Mac::e_mm_fixed: + leDstMac->setEnabled(true); + leDstMacCount->setEnabled(false); + leDstMacStep->setEnabled(false); + break; + default: + leDstMac->setEnabled(true); + leDstMacCount->setEnabled(true); + leDstMacStep->setEnabled(true); + break; } + resolveInfo->setVisible( + cmbDstMacMode->currentIndex() == OstProto::Mac::e_mm_resolve + || cmbSrcMacMode->currentIndex() == OstProto::Mac::e_mm_resolve); } void MacConfigForm::on_cmbSrcMacMode_currentIndexChanged(int index) { - if (index == OstProto::Mac::e_mm_fixed) - { - leSrcMacCount->setEnabled(false); - leSrcMacStep->setEnabled(false); - } - else - { - leSrcMacCount->setEnabled(true); - leSrcMacStep->setEnabled(true); + switch (index) { + case OstProto::Mac::e_mm_resolve: + leSrcMac->setEnabled(false); + leSrcMacCount->setEnabled(false); + leSrcMacStep->setEnabled(false); + break; + case OstProto::Mac::e_mm_fixed: + leSrcMac->setEnabled(true); + leSrcMacCount->setEnabled(false); + leSrcMacStep->setEnabled(false); + break; + default: + leSrcMac->setEnabled(true); + leSrcMacCount->setEnabled(true); + leSrcMacStep->setEnabled(true); + break; } + resolveInfo->setVisible( + cmbDstMacMode->currentIndex() == OstProto::Mac::e_mm_resolve + || cmbSrcMacMode->currentIndex() == OstProto::Mac::e_mm_resolve); } void MacConfigForm::loadWidget(AbstractProtocol *proto) diff --git a/common/macedit.h b/common/macedit.h new file mode 100644 index 0000000..390c6a3 --- /dev/null +++ b/common/macedit.h @@ -0,0 +1,54 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _MAC_EDIT_H +#define _MAC_EDIT_H + +#include + +class MacEdit: public QLineEdit +{ +public: + MacEdit(QWidget *parent = 0); + + quint64 value(); + void setValue(quint64 val); +}; + +inline MacEdit::MacEdit(QWidget *parent) + : QLineEdit(parent) +{ + QRegExp reMac("([0-9,a-f,A-F]{2,2}[:-]){5,5}[0-9,a-f,A-F]{2,2}"); + + setValidator(new QRegExpValidator(reMac, this)); +} + +inline quint64 MacEdit::value() +{ + return text().remove(QChar(':')).toULongLong(NULL, 16); +} + +inline void MacEdit::setValue(quint64 val) +{ + setText(QString("%1").arg(val, 6*2, 16, QChar('0')) + .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:").toUpper()); +} + +#endif + diff --git a/common/fileformat.cpp b/common/nativefileformat.cpp similarity index 76% rename from common/fileformat.cpp rename to common/nativefileformat.cpp index 4edd980..e2fc3eb 100644 --- a/common/fileformat.cpp +++ b/common/nativefileformat.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2010 Srivats P. +Copyright (C) 2010, 2016 Srivats P. This file is part of "Ostinato" @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ -#include "fileformat.h" +#include "nativefileformat.h" #include "crc32c.h" @@ -25,15 +25,29 @@ along with this program. If not, see #include #include -#include +#define tr(str) QObject::tr(str) -const std::string FileFormat::kFileMagicValue = "\xa7\xb7OSTINATO"; +const std::string NativeFileFormat::kFileMagicValue = "\xa7\xb7OSTINATO"; -FileFormat fileFormat; +static const int kBaseHex = 16; -const int kBaseHex = 16; +static QString fileTypeStr(OstProto::FileType fileType) +{ + switch (fileType) { + case OstProto::kReservedFileType: + return QString("Reserved"); + case OstProto::kStreamsFileType: + return QString("Streams"); + case OstProto::kSessionFileType: + return QString("Streams"); + default: + Q_ASSERT(false); + } -FileFormat::FileFormat() + return QString("Unknown"); +} + +NativeFileFormat::NativeFileFormat() { /* * We don't have any "real" work to do here in the constructor. @@ -54,20 +68,18 @@ FileFormat::FileFormat() Q_ASSERT(cksum.ByteSize() == kFileChecksumSize); } -FileFormat::~FileFormat() -{ -} - -bool FileFormat::openStreams(const QString fileName, - OstProto::StreamConfigList &streams, QString &error) +bool NativeFileFormat::open( + const QString fileName, + OstProto::FileType fileType, + OstProto::FileMeta &meta, + OstProto::FileContent &content, + QString &error) { QFile file(fileName); QByteArray buf; int size, contentOffset, contentSize; quint32 calcCksum; OstProto::FileMagic magic; - OstProto::FileMeta meta; - OstProto::FileContent content; OstProto::FileChecksum cksum, zeroCksum; if (!file.open(QIODevice::ReadOnly)) @@ -95,7 +107,7 @@ bool FileFormat::openStreams(const QString fileName, // Parse and verify magic if (!magic.ParseFromArray( - (void*)(buf.constData() + kFileMagicOffset), + (void*)(buf.constData() + kFileMagicOffset), kFileMagicSize)) { goto _magic_parse_fail; @@ -105,7 +117,7 @@ bool FileFormat::openStreams(const QString fileName, // Parse and verify checksum if (!cksum.ParseFromArray( - (void*)(buf.constData() + size - kFileChecksumSize), + (void*)(buf.constData() + size - kFileChecksumSize), kFileChecksumSize)) { goto _cksum_parse_fail; @@ -118,7 +130,7 @@ bool FileFormat::openStreams(const QString fileName, { goto _zero_cksum_serialize_fail; } - + calcCksum = checksumCrc32C((quint8*) buf.constData(), size); qDebug("checksum \nExpected:%x Actual:%x", @@ -129,17 +141,18 @@ bool FileFormat::openStreams(const QString fileName, // Parse the metadata first before we parse the full contents if (!meta.ParseFromArray( - (void*)(buf.constData() + kFileMetaDataOffset), - size - kFileMetaDataOffset)) + (void*)(buf.constData() + kFileMetaDataOffset), + fileMetaSize((quint8*)buf.constData(), size))) { goto _metadata_parse_fail; } - qDebug("%s: File MetaData (INFORMATION) - \n%s", __FUNCTION__, + qDebug("%s: File MetaData (INFORMATION) - \n%s", __FUNCTION__, QString().fromStdString(meta.DebugString()).toAscii().constData()); + qDebug("%s: END MetaData", __FUNCTION__); // MetaData Validation(s) - if (meta.data().file_type() != OstProto::kStreamsFileType) + if (meta.data().file_type() != fileType) goto _unexpected_file_type; if (meta.data().format_version_major() != kFileFormatVersionMajor) @@ -165,34 +178,26 @@ bool FileFormat::openStreams(const QString fileName, // ByteSize() does not include the Tag/Key, so we add 2 for that contentOffset = kFileMetaDataOffset + meta.data().ByteSize() + 2; contentSize = size - contentOffset - kFileChecksumSize; + qDebug("%s: content offset/size = %d/%d", __FUNCTION__, + contentOffset, contentSize); // Parse full contents if (!content.ParseFromArray( - (void*)(buf.constData() + contentOffset), + (void*)(buf.constData() + contentOffset), contentSize)) { goto _content_parse_fail; } - if (!content.matter().has_streams()) - goto _missing_streams; - - postParseFixup(meta.data(), content); - - streams.CopyFrom(content.matter().streams()); - return true; -_missing_streams: - error = QString(tr("%1 does not contain any streams")).arg(fileName); - goto _fail; _content_parse_fail: error = QString(tr("Failed parsing %1 contents")).arg(fileName); qDebug("Error: %s", QString().fromStdString( - content.matter().InitializationErrorString()) + content.InitializationErrorString()) .toAscii().constData()); qDebug("Debug: %s", QString().fromStdString( - content.matter().DebugString()).toAscii().constData()); + content.DebugString()).toAscii().constData()); goto _fail; _incompatible_file_version: error = QString(tr("%1 is in an incompatible format version - %2.%3.%4" @@ -206,7 +211,9 @@ _incompatible_file_version: .arg(kFileFormatVersionRevision); goto _fail; _unexpected_file_type: - error = QString(tr("%1 is not a streams file")).arg(fileName); + error = QString(tr("%1 is not a %2 file")) + .arg(fileName) + .arg(fileTypeStr(fileType)); goto _fail; _metadata_parse_fail: error = QString(tr("Failed parsing %1 meta data")).arg(fileName); @@ -260,12 +267,14 @@ _fail: return false; } -bool FileFormat::saveStreams(const OstProto::StreamConfigList streams, - const QString fileName, QString &error) +bool NativeFileFormat::save( + OstProto::FileType fileType, + const OstProto::FileContent &content, + const QString fileName, + QString &error) { OstProto::FileMagic magic; OstProto::FileMeta meta; - OstProto::FileContent content; OstProto::FileChecksum cksum; QFile file(fileName); int metaSize, contentSize; @@ -280,13 +289,12 @@ bool FileFormat::saveStreams(const OstProto::StreamConfigList streams, Q_ASSERT(cksum.IsInitialized()); initFileMetaData(*(meta.mutable_data())); - meta.mutable_data()->set_file_type(OstProto::kStreamsFileType); + meta.mutable_data()->set_file_type(fileType); Q_ASSERT(meta.IsInitialized()); - if (!streams.IsInitialized()) - goto _stream_not_init; + if (!content.IsInitialized()) + goto _content_not_init; - content.mutable_matter()->mutable_streams()->CopyFrom(streams); Q_ASSERT(content.IsInitialized()); metaSize = meta.ByteSize(); @@ -323,7 +331,7 @@ bool FileFormat::saveStreams(const OstProto::StreamConfigList streams, goto _zero_cksum_serialize_fail; } - emit status("Calculating checksum..."); + // TODO: emit status("Calculating checksum..."); // Calculate and write checksum calcCksum = checksumCrc32C((quint8*)buf.constData(), buf.size()); @@ -338,7 +346,7 @@ bool FileFormat::saveStreams(const OstProto::StreamConfigList streams, qDebug("Writing %d bytes", buf.size()); //qDebug("%s", QString(buf.toHex()).toAscii().constData()); - emit status("Writing to disk..."); + // TODO: emit status("Writing to disk..."); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) goto _open_fail; @@ -346,7 +354,7 @@ bool FileFormat::saveStreams(const OstProto::StreamConfigList streams, goto _write_fail; file.close(); - + return true; _write_fail: @@ -387,18 +395,20 @@ _magic_serialize_fail: magic.InitializationErrorString())) .arg(QString().fromStdString(magic.DebugString())); goto _fail; -_stream_not_init: - error = QString(tr("Internal Error: Streams not initialized\n%1\n%2")) +_content_not_init: + error = QString(tr("Internal Error: Content not initialized\n%1\n%2")) .arg(QString().fromStdString( - streams.InitializationErrorString())) - .arg(QString().fromStdString(streams.DebugString())); + content.InitializationErrorString())) + .arg(QString().fromStdString(content.DebugString())); goto _fail; _fail: qDebug("%s", error.toAscii().constData()); return false; } -bool FileFormat::isMyFileFormat(const QString fileName) +bool NativeFileFormat::isNativeFileFormat( + const QString fileName, + OstProto::FileType fileType) { bool ret = false; QFile file(fileName); @@ -408,13 +418,25 @@ bool FileFormat::isMyFileFormat(const QString fileName) if (!file.open(QIODevice::ReadOnly)) goto _exit; - buf = file.peek(kFileMagicOffset + kFileMagicSize); - if (!magic.ParseFromArray((void*)(buf.constData() + kFileMagicOffset), + // Assume tag/length for MetaData will fit in 8 bytes + buf = file.peek(kFileMagicOffset + kFileMagicSize + 8); + if (!magic.ParseFromArray((void*)(buf.constData() + kFileMagicOffset), kFileMagicSize)) goto _close_exit; - if (magic.value() == kFileMagicValue) - ret = true; + if (magic.value() == kFileMagicValue) { + OstProto::FileMeta meta; + int metaSize = fileMetaSize((quint8*)buf.constData(), buf.size()); + buf = file.peek(kFileMagicOffset + kFileMagicSize + metaSize); + if (!meta.ParseFromArray( + (void*)(buf.constData() + kFileMetaDataOffset), metaSize)) { + qDebug("%s: File MetaData\n%s", __FUNCTION__, + QString().fromStdString(meta.DebugString()).toAscii().constData()); + goto _close_exit; + } + if (meta.data().file_type() == fileType) + ret = true; + } _close_exit: file.close(); @@ -422,15 +444,7 @@ _exit: return ret; } -bool FileFormat::isMyFileType(const QString fileType) -{ - if (fileType.startsWith("Ostinato")) - return true; - else - return false; -} - -void FileFormat::initFileMetaData(OstProto::FileMetaData &metaData) +void NativeFileFormat::initFileMetaData(OstProto::FileMetaData &metaData) { // Fill in the "native" file format version metaData.set_format_version_major(kFileFormatVersionMajor); @@ -445,9 +459,53 @@ void FileFormat::initFileMetaData(OstProto::FileMetaData &metaData) qApp->property("revision").toString().toUtf8().constData()); } +int NativeFileFormat::fileMetaSize(const quint8* file, int size) +{ + int i = kFileMetaDataOffset; + uint result, shift; + const int kWireTypeLengthDelimited = 2; + + // An embedded Message field is encoded as + // + // See Protobuf Encoding for more details + + // Decode 'Key' varint + result = 0; + shift = 0; + while (i < size) { + quint8 byte = file[i++]; + result |= (byte & 0x7f) << shift; + if (!(byte & 0x80)) // MSB == 0? + break; + shift += 7; + } + + if (i >= size) + return 0; + + Q_ASSERT(result == ((OstProto::File::kMetaDataFieldNumber << 3) + | kWireTypeLengthDelimited)); + + // Decode 'Length' varint + result = 0; + shift = 0; + while (i < size) { + quint8 byte = file[i++]; + result |= (byte & 0x7f) << shift; + if (!(byte & 0x80)) // MSB == 0? + break; + shift += 7; + } + + if (i >= size) + return 0; + + return int(result+(i-kFileMetaDataOffset)); +} + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" /*! Fixup content to what is expected in the native version */ -void FileFormat::postParseFixup(OstProto::FileMetaData metaData, +void NativeFileFormat::postParseFixup(OstProto::FileMetaData metaData, OstProto::FileContent &content) { Q_ASSERT(metaData.format_version_major() == kFileFormatVersionMajor); @@ -460,7 +518,7 @@ void FileFormat::postParseFixup(OstProto::FileMetaData metaData, int n = content.matter().streams().stream_size(); for (int i = 0; i < n; i++) { - OstProto::StreamControl *sctl = + OstProto::StreamControl *sctl = content.mutable_matter()->mutable_streams()->mutable_stream(i)->mutable_control(); sctl->set_packets_per_sec(sctl->obsolete_packets_per_sec()); sctl->set_bursts_per_sec(sctl->obsolete_bursts_per_sec()); @@ -473,7 +531,7 @@ void FileFormat::postParseFixup(OstProto::FileMetaData metaData, case 0: default: - qWarning("%s: minor version %u unhandled", __FUNCTION__, + qWarning("%s: minor version %u unhandled", __FUNCTION__, metaData.format_version_minor()); Q_ASSERT_X(false, "postParseFixup", "unhandled minor version"); } diff --git a/common/fileformat.h b/common/nativefileformat.h similarity index 51% rename from common/fileformat.h rename to common/nativefileformat.h index f2c5b32..83759ae 100644 --- a/common/fileformat.h +++ b/common/nativefileformat.h @@ -1,5 +1,5 @@ /* -Copyright (C) 2010 Srivats P. +Copyright (C) 2010, 2016 Srivats P. This file is part of "Ostinato" @@ -16,31 +16,48 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ -#ifndef _FILE_FORMAT_H -#define _FILE_FORMAT_H +#ifndef _NATIVE_FILE_FORMAT_H +#define _NATIVE_FILE_FORMAT_H -#include "abstractfileformat.h" +/* + * This file contains helper functions for the native file format + * defined in fileformat.proto + * + * The actual file format classes - (Ostm)FileFormat and OssnFileFormat + * use multiple inheritance from the abstract interface class and this + * helper class + * + * The primary reason for the existence of this class is to have a common + * code for dealing with native file formats + */ #include "fileformat.pb.h" -class FileFormat : public AbstractFileFormat +#include + +class NativeFileFormat { public: - FileFormat(); - ~FileFormat(); + NativeFileFormat(); - virtual bool openStreams(const QString fileName, - OstProto::StreamConfigList &streams, QString &error); - virtual bool saveStreams(const OstProto::StreamConfigList streams, - const QString fileName, QString &error); + bool open(const QString fileName, + OstProto::FileType fileType, + OstProto::FileMeta &meta, + OstProto::FileContent &content, + QString &error); + bool save(OstProto::FileType fileType, + const OstProto::FileContent &content, + const QString fileName, + QString &error); - bool isMyFileFormat(const QString fileName); - bool isMyFileType(const QString fileType); + bool isNativeFileFormat(const QString fileName, + OstProto::FileType fileType); + void postParseFixup(OstProto::FileMetaData metaData, + OstProto::FileContent &content); private: void initFileMetaData(OstProto::FileMetaData &metaData); - void postParseFixup(OstProto::FileMetaData metaData, - OstProto::FileContent &content); + int fileMetaSize(const quint8* file, int size); static const int kFileMagicSize = 12; static const int kFileChecksumSize = 5; @@ -50,13 +67,11 @@ private: static const int kFileMetaDataOffset = kFileMagicSize; static const std::string kFileMagicValue; - + // Native file format version static const uint kFileFormatVersionMajor = 0; static const uint kFileFormatVersionMinor = 2; static const uint kFileFormatVersionRevision = 4; }; -extern FileFormat fileFormat; - #endif diff --git a/common/ossnfileformat.cpp b/common/ossnfileformat.cpp new file mode 100644 index 0000000..adb943c --- /dev/null +++ b/common/ossnfileformat.cpp @@ -0,0 +1,91 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "ossnfileformat.h" + +OssnFileFormat ossnFileFormat; + +OssnFileFormat::OssnFileFormat() + : SessionFileFormat(), NativeFileFormat() +{ + // Do Nothing +} + +bool OssnFileFormat::open(const QString fileName, + OstProto::SessionContent &session, QString &error) +{ + OstProto::FileMeta meta; + OstProto::FileContent content; + bool ret = NativeFileFormat::open(fileName, OstProto::kSessionFileType, + meta, content, error); + if (!ret) + goto _exit; + + if (!content.matter().has_session()) + goto _missing_session; + + postParseFixup(meta.data(), content); + + session.CopyFrom(content.matter().session()); + + return true; + +_missing_session: + error = QString(tr("%1 does not contain a session")).arg(fileName); + goto _fail; +_fail: + qDebug("%s", error.toAscii().constData()); +_exit: + return false; +} + +bool OssnFileFormat::save(const OstProto::SessionContent &session, + const QString fileName, QString &error) +{ + OstProto::FileContent content; + + if (!session.IsInitialized()) + goto _session_not_init; + + content.mutable_matter()->mutable_session()->CopyFrom(session); + Q_ASSERT(content.IsInitialized()); + + return NativeFileFormat::save(OstProto::kSessionFileType, content, + fileName, error); + +_session_not_init: + error = QString(tr("Internal Error: Session not initialized\n%1\n%2")) + .arg(QString().fromStdString( + session.InitializationErrorString())) + .arg(QString().fromStdString(session.DebugString())); + goto _fail; +_fail: + qDebug("%s", error.toAscii().constData()); + return false; +} + +bool OssnFileFormat::isMyFileFormat(const QString fileName) +{ + return isNativeFileFormat(fileName, OstProto::kSessionFileType); +} + +bool OssnFileFormat::isMyFileType(const QString fileType) +{ + return fileType.contains("(*.ossn)") ? true : false; +} diff --git a/common/ossnfileformat.h b/common/ossnfileformat.h new file mode 100644 index 0000000..e6412b4 --- /dev/null +++ b/common/ossnfileformat.h @@ -0,0 +1,43 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _OSSN_FILE_FORMAT_H +#define _OSSN_FILE_FORMAT_H + +#include "nativefileformat.h" +#include "sessionfileformat.h" + +class OssnFileFormat : public SessionFileFormat, public NativeFileFormat +{ +public: + OssnFileFormat(); + + virtual bool open(const QString fileName, + OstProto::SessionContent &session, QString &error); + virtual bool save(const OstProto::SessionContent &session, + const QString fileName, QString &error); + + virtual bool isMyFileFormat(const QString fileName); + virtual bool isMyFileType(const QString fileType); +}; + +extern OssnFileFormat ossnFileFormat; + +#endif + diff --git a/common/ostmfileformat.cpp b/common/ostmfileformat.cpp new file mode 100644 index 0000000..3cbd75c --- /dev/null +++ b/common/ostmfileformat.cpp @@ -0,0 +1,94 @@ +/* +Copyright (C) 2010 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "ostmfileformat.h" + +OstmFileFormat fileFormat; + +OstmFileFormat::OstmFileFormat() + : StreamFileFormat(), NativeFileFormat() +{ + // Do Nothing! +} + +bool OstmFileFormat::open(const QString fileName, + OstProto::StreamConfigList &streams, QString &error) +{ + OstProto::FileMeta meta; + OstProto::FileContent content; + bool ret = NativeFileFormat::open(fileName, OstProto::kStreamsFileType, + meta, content, error); + if (!ret) + goto _fail; + + if (!content.matter().has_streams()) + goto _missing_streams; + + postParseFixup(meta.data(), content); + + streams.CopyFrom(content.matter().streams()); + + return true; + +_missing_streams: + error = QString(tr("%1 does not contain any streams")).arg(fileName); + goto _fail; +_fail: + qDebug("%s", error.toAscii().constData()); + return false; +} + +bool OstmFileFormat::save(const OstProto::StreamConfigList streams, + const QString fileName, QString &error) +{ + OstProto::FileContent content; + + if (!streams.IsInitialized()) + goto _stream_not_init; + + content.mutable_matter()->mutable_streams()->CopyFrom(streams); + Q_ASSERT(content.IsInitialized()); + + return NativeFileFormat::save(OstProto::kStreamsFileType, content, + fileName, error); + +_stream_not_init: + error = QString(tr("Internal Error: Streams not initialized\n%1\n%2")) + .arg(QString().fromStdString( + streams.InitializationErrorString())) + .arg(QString().fromStdString(streams.DebugString())); + goto _fail; +_fail: + qDebug("%s", error.toAscii().constData()); + return false; +} + +bool OstmFileFormat::isMyFileFormat(const QString fileName) +{ + return isNativeFileFormat(fileName, OstProto::kStreamsFileType); +} + +bool OstmFileFormat::isMyFileType(const QString fileType) +{ + if (fileType.startsWith("Ostinato")) + return true; + else + return false; +} + diff --git a/common/ostmfileformat.h b/common/ostmfileformat.h new file mode 100644 index 0000000..f44ea74 --- /dev/null +++ b/common/ostmfileformat.h @@ -0,0 +1,43 @@ +/* +Copyright (C) 2010 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ +#ifndef _OSTM_FILE_FORMAT_H +#define _OSTM_FILE_FORMAT_H + +#include "nativefileformat.h" +#include "streamfileformat.h" + +#include "fileformat.pb.h" + +class OstmFileFormat : public StreamFileFormat, public NativeFileFormat +{ +public: + OstmFileFormat(); + + virtual bool open(const QString fileName, + OstProto::StreamConfigList &streams, QString &error); + virtual bool save(const OstProto::StreamConfigList streams, + const QString fileName, QString &error); + + bool isMyFileFormat(const QString fileName); + bool isMyFileType(const QString fileType); +}; + +extern OstmFileFormat fileFormat; + +#endif diff --git a/common/ostproto.pro b/common/ostproto.pro index 8d14c1a..5aff222 100644 --- a/common/ostproto.pro +++ b/common/ostproto.pro @@ -7,6 +7,9 @@ LIBS += \ PROTOS = \ protocol.proto \ + emulproto.proto + +PROTOS += \ mac.proto \ payload.proto \ eth2.proto \ @@ -35,7 +38,7 @@ PROTOS = \ textproto.proto \ userscript.proto \ hexdump.proto \ - sample.proto + sample.proto HEADERS = \ abstractprotocol.h \ diff --git a/common/ostprotogui.pro b/common/ostprotogui.pro index a91beea..7785def 100644 --- a/common/ostprotogui.pro +++ b/common/ostprotogui.pro @@ -35,16 +35,20 @@ PROTOS = \ # TODO: Move fileformat related stuff into a different library - why? HEADERS = \ ostprotolib.h \ - abstractfileformat.h \ - fileformat.h \ ipv4addressdelegate.h \ ipv6addressdelegate.h \ + nativefileformat.h \ + ossnfileformat.h \ + ostmfileformat.h \ pcapfileformat.h \ pdmlfileformat.h \ pythonfileformat.h \ pdmlprotocol.h \ pdmlprotocols.h \ - pdmlreader.h + pdmlreader.h \ + sessionfileformat.h \ + streamfileformat.h \ + spinboxdelegate.h HEADERS += \ abstractprotocolconfig.h \ @@ -79,14 +83,18 @@ HEADERS += \ SOURCES += \ ostprotolib.cpp \ - abstractfileformat.cpp \ - fileformat.cpp \ + nativefileformat.cpp \ + ossnfileformat.cpp \ + ostmfileformat.cpp \ pcapfileformat.cpp \ pdmlfileformat.cpp \ pythonfileformat.cpp \ pdmlprotocol.cpp \ pdmlprotocols.cpp \ pdmlreader.cpp \ + sessionfileformat.cpp \ + streamfileformat.cpp \ + spinboxdelegate.cpp SOURCES += \ protocolwidgetfactory.cpp \ diff --git a/common/pcapfileformat.cpp b/common/pcapfileformat.cpp index e96b684..9f352dd 100644 --- a/common/pcapfileformat.cpp +++ b/common/pcapfileformat.cpp @@ -91,7 +91,7 @@ PcapFileFormat::~PcapFileFormat() delete importDialog_; } -bool PcapFileFormat::openStreams(const QString fileName, +bool PcapFileFormat::open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { bool isOk = false; @@ -325,7 +325,7 @@ bool PcapFileFormat::openStreams(const QString fileName, goto _diff_fail; } - if (!saveStreams(streams, importedPcapFile.fileName(), error)) + if (!save(streams, importedPcapFile.fileName(), error)) { error.append("Error saving imported streams as PCAP for diff"); goto _diff_fail; @@ -553,7 +553,7 @@ bool PcapFileFormat::readPacket(PcapPacketHeader &pktHdr, QByteArray &pktBuf) return true; } -bool PcapFileFormat::saveStreams(const OstProto::StreamConfigList streams, +bool PcapFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { bool isOk = false; diff --git a/common/pcapfileformat.h b/common/pcapfileformat.h index 064aaf1..cb0c461 100644 --- a/common/pcapfileformat.h +++ b/common/pcapfileformat.h @@ -19,7 +19,7 @@ along with this program. If not, see #ifndef _PCAP_FILE_FORMAT_H #define _PCAP_FILE_FORMAT_H -#include "abstractfileformat.h" +#include "streamfileformat.h" #include "ui_pcapfileimport.h" #include @@ -39,7 +39,7 @@ private: }; class PdmlReader; -class PcapFileFormat : public AbstractFileFormat +class PcapFileFormat : public StreamFileFormat { friend class PdmlReader; @@ -47,9 +47,9 @@ public: PcapFileFormat(); ~PcapFileFormat(); - bool openStreams(const QString fileName, + bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); - bool saveStreams(const OstProto::StreamConfigList streams, + bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); virtual QDialog* openOptionsDialog(); diff --git a/common/pdmlfileformat.cpp b/common/pdmlfileformat.cpp index e567d99..f522a8c 100644 --- a/common/pdmlfileformat.cpp +++ b/common/pdmlfileformat.cpp @@ -35,7 +35,7 @@ PdmlFileFormat::~PdmlFileFormat() { } -bool PdmlFileFormat::openStreams(const QString fileName, +bool PdmlFileFormat::open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { bool isOk = false; @@ -75,12 +75,12 @@ _exit: return isOk; } -bool PdmlFileFormat::saveStreams(const OstProto::StreamConfigList streams, +bool PdmlFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { bool isOk = false; QTemporaryFile pcapFile; - AbstractFileFormat *fmt = AbstractFileFormat::fileFormatFromType("PCAP"); + StreamFileFormat *fmt = StreamFileFormat::fileFormatFromType("PCAP"); QProcess tshark; Q_ASSERT(fmt); @@ -97,7 +97,7 @@ bool PdmlFileFormat::saveStreams(const OstProto::StreamConfigList streams, connect(fmt, SIGNAL(progress(int)), this, SIGNAL(progress(int))); emit status("Writing intermediate PCAP file..."); - isOk = fmt->saveStreams(streams, pcapFile.fileName(), error); + isOk = fmt->save(streams, pcapFile.fileName(), error); qDebug("generating PDML %s", fileName.toAscii().constData()); emit status("Converting PCAP to PDML..."); diff --git a/common/pdmlfileformat.h b/common/pdmlfileformat.h index e05026a..bc1d15b 100644 --- a/common/pdmlfileformat.h +++ b/common/pdmlfileformat.h @@ -19,17 +19,17 @@ along with this program. If not, see #ifndef _PDML_FILE_FORMAT_H #define _PDML_FILE_FORMAT_H -#include "abstractfileformat.h" +#include "streamfileformat.h" -class PdmlFileFormat : public AbstractFileFormat +class PdmlFileFormat : public StreamFileFormat { public: PdmlFileFormat(); ~PdmlFileFormat(); - virtual bool openStreams(const QString fileName, + virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); - virtual bool saveStreams(const OstProto::StreamConfigList streams, + virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool isMyFileFormat(const QString fileName); diff --git a/common/protocol.proto b/common/protocol.proto index fdfb5bd..439d28f 100644 --- a/common/protocol.proto +++ b/common/protocol.proto @@ -274,6 +274,55 @@ message Notification { optional PortIdList port_id_list = 6; } + +/* + * Protocol Emulation + */ +message DeviceGroupId { + required uint32 id = 1; +} + +message DeviceGroupCore { + optional string name = 1; +} + +message DeviceGroupIdList { + required PortId port_id = 1; + repeated DeviceGroupId device_group_id = 2; +} + +message EncapEmulation { + // Encap Protocols implemented as extensions + extensions 1000 to 1999; +} + +message DeviceGroup { + required DeviceGroupId device_group_id = 1; + optional DeviceGroupCore core = 2; + optional EncapEmulation encap = 3; + optional uint32 device_count = 4 [default = 1]; // per-encap + + // Device Protocols implemented as extensions + extensions 2000 to 5999; +} + +message DeviceGroupConfigList { + required PortId port_id = 1; + repeated DeviceGroup device_group = 2; +} + +message PortDeviceList { + required PortId port_id = 1; + + extensions 100 to 199; +} + +message PortNeighborList { + required PortId port_id = 1; + + extensions 100 to 199; +} + service OstService { rpc getPortIdList(Void) returns (PortIdList); rpc getPortConfig(PortIdList) returns (PortConfigList); @@ -296,5 +345,18 @@ service OstService { rpc clearStats(PortIdList) returns (Ack); rpc checkVersion(VersionInfo) returns (VersionCompatibility); + + // Device Emulation + rpc getDeviceGroupIdList(PortId) returns (DeviceGroupIdList); + rpc getDeviceGroupConfig(DeviceGroupIdList) returns (DeviceGroupConfigList); + rpc addDeviceGroup(DeviceGroupIdList) returns (Ack); + rpc deleteDeviceGroup(DeviceGroupIdList) returns (Ack); + rpc modifyDeviceGroup(DeviceGroupConfigList) returns (Ack); + + rpc getDeviceList(PortId) returns (PortDeviceList); + + rpc resolveDeviceNeighbors(PortIdList) returns (Ack); + rpc clearDeviceNeighbors(PortIdList) returns (Ack); + rpc getDeviceNeighbors(PortId) returns (PortNeighborList); } diff --git a/common/pythonfileformat.cpp b/common/pythonfileformat.cpp index cec283b..d9e65ba 100644 --- a/common/pythonfileformat.cpp +++ b/common/pythonfileformat.cpp @@ -46,14 +46,14 @@ PythonFileFormat::~PythonFileFormat() // Nothing to do } -bool PythonFileFormat::openStreams(const QString /*fileName*/, +bool PythonFileFormat::open(const QString /*fileName*/, OstProto::StreamConfigList &/*streams*/, QString &/*error*/) { // NOT SUPPORTED! return false; } -bool PythonFileFormat::saveStreams(const OstProto::StreamConfigList streams, +bool PythonFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { QFile file(fileName); diff --git a/common/pythonfileformat.h b/common/pythonfileformat.h index 55a6452..ee8e4bf 100644 --- a/common/pythonfileformat.h +++ b/common/pythonfileformat.h @@ -20,19 +20,19 @@ along with this program. If not, see #ifndef _PYTHON_FILE_FORMAT_H #define _PYTHON_FILE_FORMAT_H -#include "abstractfileformat.h" +#include "streamfileformat.h" #include -class PythonFileFormat : public AbstractFileFormat +class PythonFileFormat : public StreamFileFormat { public: PythonFileFormat(); ~PythonFileFormat(); - virtual bool openStreams(const QString fileName, + virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); - virtual bool saveStreams(const OstProto::StreamConfigList streams, + virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool isMyFileFormat(const QString fileName); diff --git a/common/sessionfileformat.cpp b/common/sessionfileformat.cpp new file mode 100644 index 0000000..9e14530 --- /dev/null +++ b/common/sessionfileformat.cpp @@ -0,0 +1,117 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "sessionfileformat.h" + +#include "ossnfileformat.h" + +#include + +SessionFileFormat::SessionFileFormat() +{ + stop_ = false; +} + +SessionFileFormat::~SessionFileFormat() +{ +} + +QDialog* SessionFileFormat::openOptionsDialog() +{ + return NULL; +} + +QDialog* SessionFileFormat::saveOptionsDialog() +{ + return NULL; +} + +QStringList SessionFileFormat::supportedFileTypes(Operation op) +{ + QStringList fileTypes; + + fileTypes << "Ostinato Session (*.ossn)"; + + if (op == kOpenFile) + fileTypes << "All files (*)"; + + return fileTypes; +} + +void SessionFileFormat::openAsync(const QString fileName, + OstProto::SessionContent &session, QString &error) +{ + fileName_ = fileName; + openSession_ = &session; + error_ = &error; + op_ = kOpenFile; + stop_ = false; + + start(); +} + +void SessionFileFormat::saveAsync( + const OstProto::SessionContent &session, + const QString fileName, QString &error) +{ + saveSession_ = &session; + fileName_ = fileName; + error_ = &error; + op_ = kSaveFile; + stop_ = false; + + start(); +} + +bool SessionFileFormat::result() +{ + return result_; +} + +SessionFileFormat* SessionFileFormat::fileFormatFromFile( + const QString fileName) +{ + if (ossnFileFormat.isMyFileFormat(fileName)) + return &ossnFileFormat; + + return NULL; +} + +SessionFileFormat* SessionFileFormat::fileFormatFromType( + const QString fileType) +{ + + if (ossnFileFormat.isMyFileType(fileType)) + return &ossnFileFormat; + + return NULL; +} + +void SessionFileFormat::cancel() +{ + stop_ = true; +} + +void SessionFileFormat::run() +{ + if (op_ == kOpenFile) + result_ = open(fileName_, *openSession_, *error_); + else if (op_ == kSaveFile) + result_ = save(*saveSession_, fileName_, *error_); +} diff --git a/common/sessionfileformat.h b/common/sessionfileformat.h new file mode 100644 index 0000000..31fe16c --- /dev/null +++ b/common/sessionfileformat.h @@ -0,0 +1,87 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _SESSION_FILE_FORMAT_H +#define _SESSION_FILE_FORMAT_H + +#include "fileformat.pb.h" +#include "protocol.pb.h" + +#include +#include + +class QDialog; + +class SessionFileFormat : public QThread +{ + Q_OBJECT +public: + enum Operation { kOpenFile, kSaveFile }; + + SessionFileFormat(); + virtual ~SessionFileFormat(); + + virtual bool open(const QString fileName, + OstProto::SessionContent &session, QString &error) = 0; + virtual bool save(const OstProto::SessionContent &session, + const QString fileName, QString &error) = 0; + + virtual QDialog* openOptionsDialog(); + virtual QDialog* saveOptionsDialog(); + + void openAsync(const QString fileName, + OstProto::SessionContent &session, QString &error); + void saveAsync(const OstProto::SessionContent &session, + const QString fileName, QString &error); + + bool result(); + + static QStringList supportedFileTypes(Operation op); + + static SessionFileFormat* fileFormatFromFile(const QString fileName); + static SessionFileFormat* fileFormatFromType(const QString fileType); + + virtual bool isMyFileFormat(const QString fileName) = 0; + virtual bool isMyFileType(const QString fileType) = 0; + +signals: + void status(QString text); + void target(int value); + void progress(int value); + +public slots: + void cancel(); + +protected: + void run(); + + bool stop_; + +private: + QString fileName_; + OstProto::SessionContent *openSession_; + const OstProto::SessionContent *saveSession_; + QString *error_; + Operation op_; + bool result_; + +}; + +#endif + diff --git a/common/spinboxdelegate.cpp b/common/spinboxdelegate.cpp new file mode 100644 index 0000000..d0376c2 --- /dev/null +++ b/common/spinboxdelegate.cpp @@ -0,0 +1,112 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see + +This file incorporates work covered by the following copyright and +permission notice: + + Copyright (C) 2015 The Qt Company Ltd. + Contact: http://www.qt.io/licensing/ + + This file is part of the examples of the Qt Toolkit. + + $QT_BEGIN_LICENSE:BSD$ + You may use this file under the terms of the BSD license as follows: + + "Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + Neither the name of The Qt Company Ltd nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + + $QT_END_LICENSE$ +*/ + +//#include + +#include "spinboxdelegate.h" + +#include + +SpinBoxDelegate::SpinBoxDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +QWidget *SpinBoxDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &/* option */, + const QModelIndex &index) const +{ + QSpinBox *editor = new QSpinBox(parent); + editor->setMinimum(colMin_.contains(index.column()) ? + colMin_.value(index.column()) : 0); + editor->setMaximum(colMax_.contains(index.column()) ? + colMax_.value(index.column()) : INT_MAX); + + return editor; +} + +void SpinBoxDelegate::setColumnRange(int col, int min, int max) +{ + colMin_.insert(col, min); + colMax_.insert(col, max); +} + +void SpinBoxDelegate::setEditorData(QWidget *editor, + const QModelIndex &index) const +{ + int value = index.model()->data(index, Qt::EditRole).toInt(); + + QSpinBox *spinBox = static_cast(editor); + spinBox->setValue(value); +} + +void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + QSpinBox *spinBox = static_cast(editor); + spinBox->interpretText(); + int value = spinBox->value(); + + model->setData(index, value, Qt::EditRole); +} + +void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex &/* index */) const +{ + editor->setGeometry(option.rect); +} diff --git a/common/spinboxdelegate.h b/common/spinboxdelegate.h new file mode 100644 index 0000000..a253623 --- /dev/null +++ b/common/spinboxdelegate.h @@ -0,0 +1,93 @@ +/* +Copyright (C) 2016 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see + +This file incorporates work covered by the following copyright and +permission notice: + + Copyright (C) 2015 The Qt Company Ltd. + Contact: http://www.qt.io/licensing/ + + This file is part of the examples of the Qt Toolkit. + + $QT_BEGIN_LICENSE:BSD$ + You may use this file under the terms of the BSD license as follows: + + "Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + Neither the name of The Qt Company Ltd nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + + $QT_END_LICENSE$ +*/ + +#ifndef _SPIN_BOX_DELEGATE_H +#define _SPIN_BOX_DELEGATE_H + +#include +#include +#include +#include +#include +#include + +class SpinBoxDelegate : public QItemDelegate +{ + Q_OBJECT + +public: + SpinBoxDelegate(QObject *parent = 0); + + void setColumnRange(int col, int min, int max); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const; + + void setEditorData(QWidget *editor, const QModelIndex &index) const; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const; + + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex &index) const; +private: + QHash colMin_; + QHash colMax_; +}; + +#endif + diff --git a/common/streambase.cpp b/common/streambase.cpp index 361e634..622e3d5 100644 --- a/common/streambase.cpp +++ b/common/streambase.cpp @@ -24,8 +24,11 @@ along with this program. If not, see #include "protocolmanager.h" extern ProtocolManager *OstProtocolManager; +extern quint64 getDeviceMacAddress(int portId, int streamId, int frameIndex); +extern quint64 getNeighborMacAddress(int portId, int streamId, int frameIndex); -StreamBase::StreamBase() : +StreamBase::StreamBase(int portId) : + portId_(portId), mStreamId(new OstProto::StreamId), mCore(new OstProto::StreamCore), mControl(new OstProto::StreamControl) @@ -522,18 +525,22 @@ int StreamBase::frameCount() const return count; } +// Returns packet length - if bufMaxSize < frameLen(), returns truncated +// length i.e. bufMaxSize int StreamBase::frameValue(uchar *buf, int bufMaxSize, int frameIndex) const { - int pktLen, len = 0; + int maxSize, size, pktLen, len = 0; pktLen = frameLen(frameIndex); // pktLen is adjusted for CRC/FCS which will be added by the NIC pktLen -= kFcsSize; - if ((pktLen < 0) || (pktLen > bufMaxSize)) + if (pktLen <= 0) return 0; + maxSize = qMin(pktLen, bufMaxSize); + ProtocolListIterator *iter; iter = createProtocolListIterator(); @@ -545,17 +552,33 @@ int StreamBase::frameValue(uchar *buf, int bufMaxSize, int frameIndex) const proto = iter->next(); ba = proto->protocolFrameValue(frameIndex); - if (len + ba.size() < bufMaxSize) - memcpy(buf+len, ba.constData(), ba.size()); - len += ba.size(); + size = qMin(ba.size(), maxSize-len); + memcpy(buf+len, ba.constData(), size); + len += size; + + if (len == maxSize) + break; } delete iter; - // Pad with zero, if required - if (len < pktLen) - memset(buf+len, 0, pktLen-len); + // Pad with zero, if required and if we have space + if (len < maxSize) { + size = maxSize-len; + memset(buf+len, 0, size); + len += size; + } - return pktLen; + return len; +} + +quint64 StreamBase::deviceMacAddress(int frameIndex) const +{ + return getDeviceMacAddress(portId_, int(mStreamId->id()), frameIndex); +} + +quint64 StreamBase::neighborMacAddress(int frameIndex) const +{ + return getNeighborMacAddress(portId_, int(mStreamId->id()), frameIndex); } bool StreamBase::preflightCheck(QString &result) const diff --git a/common/streambase.h b/common/streambase.h index 11ac2c2..ca6e6be 100644 --- a/common/streambase.h +++ b/common/streambase.h @@ -33,15 +33,8 @@ class ProtocolListIterator; class StreamBase { -private: - OstProto::StreamId *mStreamId; - OstProto::StreamCore *mCore; - OstProto::StreamControl *mControl; - - ProtocolList *currentFrameProtocols; - public: - StreamBase(); + StreamBase(int portId = -1); ~StreamBase(); void protoDataCopyFrom(const OstProto::Stream &stream); @@ -143,9 +136,22 @@ public: int frameProtocolLength(int frameIndex) const; int frameCount() const; int frameValue(uchar *buf, int bufMaxSize, int frameIndex) const; + + quint64 deviceMacAddress(int frameIndex) const; + quint64 neighborMacAddress(int frameIndex) const; + bool preflightCheck(QString &result) const; static bool StreamLessThan(StreamBase* stream1, StreamBase* stream2); + +private: + int portId_; + + OstProto::StreamId *mStreamId; + OstProto::StreamCore *mCore; + OstProto::StreamControl *mControl; + + ProtocolList *currentFrameProtocols; }; #endif diff --git a/common/abstractfileformat.cpp b/common/streamfileformat.cpp similarity index 65% rename from common/abstractfileformat.cpp rename to common/streamfileformat.cpp index 234795a..d00dda6 100644 --- a/common/abstractfileformat.cpp +++ b/common/streamfileformat.cpp @@ -17,74 +17,82 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ -#include "abstractfileformat.h" +#include "streamfileformat.h" -#include "fileformat.h" +#include "ostmfileformat.h" #include "pcapfileformat.h" #include "pdmlfileformat.h" #include "pythonfileformat.h" #include -AbstractFileFormat::AbstractFileFormat() +StreamFileFormat::StreamFileFormat() { stop_ = false; } -AbstractFileFormat::~AbstractFileFormat() +StreamFileFormat::~StreamFileFormat() { } -QDialog* AbstractFileFormat::openOptionsDialog() +QDialog* StreamFileFormat::openOptionsDialog() { return NULL; } -QDialog* AbstractFileFormat::saveOptionsDialog() +QDialog* StreamFileFormat::saveOptionsDialog() { return NULL; } -QStringList AbstractFileFormat::supportedFileTypes() +QStringList StreamFileFormat::supportedFileTypes(Operation op) { - return QStringList() - << "Ostinato (*)" + QStringList fileTypes; + + fileTypes + << "Ostinato (*.ostm)" << "PCAP (*)" - << "PDML (*.pdml)" - << "PythonScript (*.py)"; + << "PDML (*.pdml)"; + + if (op == kSaveFile) + fileTypes << "PythonScript (*.py)"; + else if (op == kOpenFile) + fileTypes << "All files (*)"; + + return fileTypes; } -void AbstractFileFormat::openStreamsOffline(const QString fileName, +void StreamFileFormat::openAsync(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { fileName_ = fileName; openStreams_ = &streams; error_ = &error; - op_ = kOpen; + op_ = kOpenFile; stop_ = false; start(); } -void AbstractFileFormat::saveStreamsOffline( +void StreamFileFormat::saveAsync( const OstProto::StreamConfigList streams, const QString fileName, QString &error) { saveStreams_ = streams; fileName_ = fileName; error_ = &error; - op_ = kSave; + op_ = kSaveFile; stop_ = false; start(); } -bool AbstractFileFormat::result() +bool StreamFileFormat::result() { return result_; } -AbstractFileFormat* AbstractFileFormat::fileFormatFromFile( +StreamFileFormat* StreamFileFormat::fileFormatFromFile( const QString fileName) { if (fileFormat.isMyFileFormat(fileName)) @@ -99,7 +107,7 @@ AbstractFileFormat* AbstractFileFormat::fileFormatFromFile( return NULL; } -AbstractFileFormat* AbstractFileFormat::fileFormatFromType( +StreamFileFormat* StreamFileFormat::fileFormatFromType( const QString fileType) { @@ -118,15 +126,15 @@ AbstractFileFormat* AbstractFileFormat::fileFormatFromType( return NULL; } -void AbstractFileFormat::cancel() +void StreamFileFormat::cancel() { stop_ = true; } -void AbstractFileFormat::run() +void StreamFileFormat::run() { - if (op_ == kOpen) - result_ = openStreams(fileName_, *openStreams_, *error_); - else if (op_ == kSave) - result_ = saveStreams(saveStreams_, fileName_, *error_); + if (op_ == kOpenFile) + result_ = open(fileName_, *openStreams_, *error_); + else if (op_ == kSaveFile) + result_ = save(saveStreams_, fileName_, *error_); } diff --git a/common/abstractfileformat.h b/common/streamfileformat.h similarity index 70% rename from common/abstractfileformat.h rename to common/streamfileformat.h index 1f8447d..474ab09 100644 --- a/common/abstractfileformat.h +++ b/common/streamfileformat.h @@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ -#ifndef _ABSTRACT_FILE_FORMAT_H -#define _ABSTRACT_FILE_FORMAT_H +#ifndef _STREAM_FILE_FORMAT_H +#define _STREAM_FILE_FORMAT_H #include "protocol.pb.h" @@ -27,32 +27,34 @@ along with this program. If not, see class QDialog; -class AbstractFileFormat : public QThread +class StreamFileFormat : public QThread { Q_OBJECT public: - AbstractFileFormat(); - virtual ~AbstractFileFormat(); + enum Operation { kOpenFile, kSaveFile }; - virtual bool openStreams(const QString fileName, + StreamFileFormat(); + virtual ~StreamFileFormat(); + + virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) = 0; - virtual bool saveStreams(const OstProto::StreamConfigList streams, + virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) = 0; virtual QDialog* openOptionsDialog(); virtual QDialog* saveOptionsDialog(); - void openStreamsOffline(const QString fileName, + void openAsync(const QString fileName, OstProto::StreamConfigList &streams, QString &error); - void saveStreamsOffline(const OstProto::StreamConfigList streams, + void saveAsync(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool result(); - static QStringList supportedFileTypes(); + static QStringList supportedFileTypes(Operation op); - static AbstractFileFormat* fileFormatFromFile(const QString fileName); - static AbstractFileFormat* fileFormatFromType(const QString fileType); + static StreamFileFormat* fileFormatFromFile(const QString fileName); + static StreamFileFormat* fileFormatFromType(const QString fileType); #if 0 bool isMyFileFormat(const QString fileName) = 0; @@ -73,16 +75,11 @@ protected: bool stop_; private: - enum kOp - { - kOpen, - kSave - }; QString fileName_; OstProto::StreamConfigList *openStreams_; OstProto::StreamConfigList saveStreams_; QString *error_; - kOp op_; + Operation op_; bool result_; }; diff --git a/common/uint128.h b/common/uint128.h new file mode 100644 index 0000000..d39e133 --- /dev/null +++ b/common/uint128.h @@ -0,0 +1,185 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _UINT128_H +#define _UINT128_H + +#include + +#include +#include + +class UInt128 +{ +public: + UInt128(); + UInt128(quint64 hi, quint64 lo); + UInt128(quint8 *value); + + quint64 hi64() const; + quint64 lo64() const; + quint8* toArray() const; + + bool operator==(const UInt128 &other) const; + bool operator!=(const UInt128 &other) const; + UInt128 operator+(const UInt128 &other) const; + UInt128 operator*(const uint &other) const; + UInt128 operator<<(const int &shift) const; + UInt128 operator~() const; + UInt128 operator&(const UInt128 &other) const; + UInt128 operator|(const UInt128 &other) const; + +private: + quint64 hi_; + quint64 lo_; + quint8 array_[16]; +}; + +inline UInt128::UInt128() +{ + // Do nothing - value will be garbage like any other uint +} + +inline UInt128::UInt128(quint64 hi, quint64 lo) +{ + hi_ = hi; + lo_ = lo; +} + +inline UInt128::UInt128(quint8 *value) +{ + hi_ = (quint64(value[0]) << 56) + | (quint64(value[1]) << 48) + | (quint64(value[2]) << 40) + | (quint64(value[3]) << 32) + | (quint64(value[4]) << 24) + | (quint64(value[5]) << 16) + | (quint64(value[6]) << 8) + | (quint64(value[7]) << 0); + + lo_ = (quint64(value[ 8]) << 56) + | (quint64(value[ 9]) << 48) + | (quint64(value[10]) << 40) + | (quint64(value[11]) << 32) + | (quint64(value[12]) << 24) + | (quint64(value[13]) << 16) + | (quint64(value[14]) << 8) + | (quint64(value[15]) << 0); +} + +inline quint64 UInt128::hi64() const +{ + return hi_; +} + +inline quint64 UInt128::lo64() const +{ + return lo_; +} + +inline quint8* UInt128::toArray() const +{ + *(quint64*)(array_ + 0) = qToBigEndian(hi_); + *(quint64*)(array_ + 8) = qToBigEndian(lo_); + + return (quint8*)array_; +} + +inline bool UInt128::operator==(const UInt128 &other) const +{ + return ((hi_ == other.hi_) && (lo_ == other.lo_)); +} + +inline bool UInt128::operator!=(const UInt128 &other) const +{ + return ((hi_ != other.hi_) || (lo_ != other.lo_)); +} + +inline UInt128 UInt128::operator+(const UInt128 &other) const +{ + UInt128 sum; + + sum.lo_ = lo_ + other.lo_; + sum.hi_ = hi_ + other.hi_ + (sum.lo_ < lo_); + + return sum; +} + +inline UInt128 UInt128::operator*(const uint &other) const +{ + UInt128 product; + + // FIXME + product.hi_ = 0; + product.lo_ = lo_ * other; + + return product; +} + +inline UInt128 UInt128::operator<<(const int &shift) const +{ + UInt128 shifted; + + if (shift < 64) + return UInt128((hi_<>(64-shift)), lo_ << shift); + + return UInt128(hi_<<(shift-64), 0); +} + +inline UInt128 UInt128::operator~() const +{ + return UInt128(~hi_, ~lo_); +} + +inline UInt128 UInt128::operator&(const UInt128 &other) const +{ + return UInt128(hi_ & other.hi_, lo_ & other.lo_); +} + +inline UInt128 UInt128::operator|(const UInt128 &other) const +{ + return UInt128(hi_ | other.hi_, lo_ | other.lo_); +} + +template <> inline UInt128 qFromBigEndian(const uchar *src) +{ + quint64 hi, lo; + + hi = qFromBigEndian(src); + lo = qFromBigEndian(src+8); + + return UInt128(hi, lo); +} + +template <> inline UInt128 qToBigEndian(const UInt128 src) +{ + quint64 hi, lo; + + hi = qToBigEndian(src.hi64()); + lo = qToBigEndian(src.lo64()); + + return UInt128(hi, lo); +} + +inline uint qHash(const UInt128 &key) +{ + return qHash(key.hi64()) ^ qHash(key.lo64()); +} + +#endif diff --git a/rpc/pbrpcchannel.cpp b/rpc/pbrpcchannel.cpp index 6719351..dff54b5 100644 --- a/rpc/pbrpcchannel.cpp +++ b/rpc/pbrpcchannel.cpp @@ -32,6 +32,7 @@ PbRpcChannel::PbRpcChannel(QString serverName, quint16 port, isPending = false; pendingMethodId = -1; // don't care as long as isPending is false + method = NULL; controller = NULL; done = NULL; response = NULL; @@ -107,9 +108,10 @@ void PbRpcChannel::CallMethod( { RpcCall call; qDebug("RpcChannel: queueing rpc since method %d is pending;<----\n " - "queued method = %d\n" + "queued method = %d:%s\n" "queued message = \n%s\n---->", - pendingMethodId, method->index(), req->DebugString().c_str()); + pendingMethodId, method->index(), method->name().c_str(), + req->DebugString().c_str()); call.method = method; call.controller = controller; @@ -128,7 +130,8 @@ void PbRpcChannel::CallMethod( if (!req->IsInitialized()) { qWarning("RpcChannel: missing required fields in request <----"); - qDebug("req = \n%s", req->DebugString().c_str()); + qDebug("req = %s\n%s", method->input_type()->name().c_str(), + req->DebugString().c_str()); qDebug("error = \n%s\n--->", req->InitializationErrorString().c_str()); controller->SetFailed("Required fields missing"); @@ -137,6 +140,7 @@ void PbRpcChannel::CallMethod( } pendingMethodId = method->index(); + this->method=method; this->controller=controller; this->done=done; this->response=response; @@ -153,8 +157,10 @@ void PbRpcChannel::CallMethod( qDebug("client(%s) sending %d bytes <----", __FUNCTION__, PB_HDR_SIZE + len); BUFDUMP(msg, PB_HDR_SIZE); - qDebug("method = %d\n req = \n%s\n---->", - method->index(), req->DebugString().c_str()); + qDebug("method = %d:%s\n req = %s\n%s\n---->", + method->index(), method->name().c_str(), + method->input_type()->name().c_str(), + req->DebugString().c_str()); } mpSocket->write(msg, PB_HDR_SIZE); @@ -308,14 +314,18 @@ _top: if (method != 13) { qDebug("client(%s): Received Msg <---- ", __FUNCTION__); - qDebug("method = %d\nresp = \n%s\n---->", - method, response->DebugString().c_str()); + qDebug("method = %d:%s\nresp = %s\n%s\n---->", + method, this->method->name().c_str(), + this->method->output_type()->name().c_str(), + response->DebugString().c_str()); } if (!response->IsInitialized()) { qWarning("RpcChannel: missing required fields in response <----"); - qDebug("resp = \n%s", response->DebugString().c_str()); + qDebug("resp = %s\n%s", + this->method->output_type()->name().c_str(), + response->DebugString().c_str()); qDebug("error = \n%s\n--->", response->InitializationErrorString().c_str()); @@ -419,6 +429,7 @@ _top: done->Run(); pendingMethodId = -1; + this->method = NULL; controller = NULL; response = NULL; isPending = false; @@ -428,9 +439,11 @@ _top: { RpcCall call = pendingCallList.takeFirst(); qDebug("RpcChannel: executing queued method <----\n" - "method = %d\n" - "req = \n%s\n---->", - call.method->index(), call.request->DebugString().c_str()); + "method = %d:%s\n" + "req = %s\n%s\n---->", + call.method->index(), call.method->name().c_str(), + call.method->input_type()->name().c_str(), + call.request->DebugString().c_str()); CallMethod(call.method, call.controller, call.request, call.response, call.done); } @@ -475,6 +488,7 @@ void PbRpcChannel::on_mpSocket_disconnected() qDebug("In %s", __FUNCTION__); pendingMethodId = -1; + method = NULL; controller = NULL; response = NULL; isPending = false; diff --git a/rpc/pbrpcchannel.h b/rpc/pbrpcchannel.h index 5c598f4..8c77412 100644 --- a/rpc/pbrpcchannel.h +++ b/rpc/pbrpcchannel.h @@ -49,6 +49,7 @@ class PbRpcChannel : public QObject, public ::google::protobuf::RpcChannel /*! \todo (MED) : change controller, done and response to references instead of pointers? */ + const ::google::protobuf::MethodDescriptor *method; ::google::protobuf::RpcController *controller; ::google::protobuf::Closure *done; ::google::protobuf::Message *response; diff --git a/rpc/rpcconn.cpp b/rpc/rpcconn.cpp index bd42c3c..db1c428 100644 --- a/rpc/rpcconn.cpp +++ b/rpc/rpcconn.cpp @@ -28,6 +28,7 @@ along with this program. If not, see #include #include +#include #include #include #include @@ -82,6 +83,7 @@ void RpcConnection::start() return; } qDebug("clientSock Thread = %p", clientSock->thread()); + qsrand(QDateTime::currentDateTime().toTime_t()); connId.setLocalData(new QString(id.arg(clientSock->peerAddress().toString()) .arg(clientSock->peerPort()))); diff --git a/server/abstractport.cpp b/server/abstractport.cpp index 8c7d747..0a97ff9 100644 --- a/server/abstractport.cpp +++ b/server/abstractport.cpp @@ -21,8 +21,10 @@ along with this program. If not, see #include "abstractport.h" -#include "../common/streambase.h" #include "../common/abstractprotocol.h" +#include "../common/streambase.h" +#include "devicemanager.h" +#include "packetbuffer.h" #include #include @@ -47,6 +49,8 @@ AbstractPort::AbstractPort(int id, const char *device) linkState_ = OstProto::LinkStateUnknown; minPacketSetSize_ = 1; + deviceManager_ = new DeviceManager(this); + maxStatsValue_ = ULLONG_MAX; // assume 64-bit stats memset((void*) &stats_, 0, sizeof(stats_)); resetStats(); @@ -54,6 +58,7 @@ AbstractPort::AbstractPort(int id, const char *device) AbstractPort::~AbstractPort() { + delete deviceManager_; } void AbstractPort::init() @@ -84,6 +89,11 @@ bool AbstractPort::modify(const OstProto::Port &port) return ret; } +DeviceManager* AbstractPort::deviceManager() +{ + return deviceManager_; +} + StreamBase* AbstractPort::streamAtIndex(int index) { Q_ASSERT(index < streamList_.size()); @@ -581,8 +591,8 @@ void AbstractPort::stats(PortStats *stats) stats->rxBytes = (stats_.rxBytes >= epochStats_.rxBytes) ? stats_.rxBytes - epochStats_.rxBytes : stats_.rxBytes + (maxStatsValue_ - epochStats_.rxBytes); - stats->rxPps = stats_.rxPps; - stats->rxBps = stats_.rxBps; + stats->rxPps = stats_.rxPps; + stats->rxBps = stats_.rxBps; stats->txPkts = (stats_.txPkts >= epochStats_.txPkts) ? stats_.txPkts - epochStats_.txPkts : @@ -590,8 +600,8 @@ void AbstractPort::stats(PortStats *stats) stats->txBytes = (stats_.txBytes >= epochStats_.txBytes) ? stats_.txBytes - epochStats_.txBytes : stats_.txBytes + (maxStatsValue_ - epochStats_.txBytes); - stats->txPps = stats_.txPps; - stats->txBps = stats_.txBps; + stats->txPps = stats_.txPps; + stats->txBps = stats_.txBps; stats->rxDrops = (stats_.rxDrops >= epochStats_.rxDrops) ? stats_.rxDrops - epochStats_.rxDrops : @@ -606,3 +616,82 @@ void AbstractPort::stats(PortStats *stats) stats_.rxFrameErrors - epochStats_.rxFrameErrors : stats_.rxFrameErrors + (maxStatsValue_ - epochStats_.rxFrameErrors); } + +void AbstractPort::clearDeviceNeighbors() +{ + deviceManager_->clearDeviceNeighbors(); + isSendQueueDirty_ = true; +} + +void AbstractPort::resolveDeviceNeighbors() +{ + // For a user triggered 'Resolve Neighbors', the behaviour we want is + // IP not in cache - send ARP/NDP request + // IP present in cache, but unresolved - re-send ARP/NDP request + // IP present in cache and resolved - don't sent ARP/NDP + // + // Device does not resend ARP/NDP requests if the IP address is + // already present in the cache, irrespective of whether it is + // resolved or not (this is done to avoid sending duplicate requests). + // + // So, to get the behaviour we want, let's clear all unresolved neighbors + // before calling resolve + deviceManager_->clearDeviceNeighbors(Device::kUnresolvedNeighbors); + + // Resolve gateway for each device first ... + deviceManager_->resolveDeviceGateways(); + + // ... then resolve neighbor for each unique frame of each stream + // NOTE: + // 1. All the frames may have the same destination ip,but may have + // different source ip so may belong to a different emulated device; + // so we cannot optimize and send only one ARP + // 2. For a unidirectional stream, at egress, this will create ARP + // entries on the DUT for each of the source addresses + // + // TODO(optimization): Identify if stream does not vary in srcIp or dstIp + // - in which case resolve for only one frame of the stream + for (int i = 0; i < streamList_.size(); i++) + { + const StreamBase *stream = streamList_.at(i); + int frameCount = stream->frameVariableCount(); + + for (int j = 0; j < frameCount; j++) { + // we need the packet contents only uptil the L3 header + int pktLen = stream->frameValue(pktBuf_, kMaxL3PktSize, j); + if (pktLen) { + PacketBuffer pktBuf(pktBuf_, pktLen); + deviceManager_->resolveDeviceNeighbor(&pktBuf); + } + } + } + isSendQueueDirty_ = true; +} + +quint64 AbstractPort::deviceMacAddress(int streamId, int frameIndex) +{ + // we need the packet contents only uptil the L3 header + StreamBase *s = stream(streamId); + int pktLen = s->frameValue(pktBuf_, kMaxL3PktSize, frameIndex); + + if (pktLen) { + PacketBuffer pktBuf(pktBuf_, pktLen); + return deviceManager_->deviceMacAddress(&pktBuf); + } + + return 0; +} + +quint64 AbstractPort::neighborMacAddress(int streamId, int frameIndex) +{ + // we need the packet contents only uptil the L3 header + StreamBase *s = stream(streamId); + int pktLen = s->frameValue(pktBuf_, kMaxL3PktSize, frameIndex); + + if (pktLen) { + PacketBuffer pktBuf(pktBuf_, pktLen); + return deviceManager_->neighborMacAddress(&pktBuf); + } + + return 0; +} diff --git a/server/abstractport.h b/server/abstractport.h index 3e99cb9..939e701 100644 --- a/server/abstractport.h +++ b/server/abstractport.h @@ -25,9 +25,14 @@ along with this program. If not, see #include "../common/protocol.pb.h" +class DeviceManager; class StreamBase; +class PacketBuffer; class QIODevice; +// TODO: send notification back to client(s) +#define notify qWarning + class AbstractPort { public: @@ -106,6 +111,17 @@ public: void stats(PortStats *stats); void resetStats() { epochStats_ = stats_; } + DeviceManager* deviceManager(); + virtual void startDeviceEmulation() = 0; + virtual void stopDeviceEmulation() = 0; + virtual int sendEmulationPacket(PacketBuffer *pktBuf) = 0; + + void clearDeviceNeighbors(); + void resolveDeviceNeighbors(); + + quint64 deviceMacAddress(int streamId, int frameIndex); + quint64 neighborMacAddress(int streamId, int frameIndex); + protected: void addNote(QString note); @@ -122,12 +138,20 @@ protected: struct PortStats stats_; //! \todo Need lock for stats access/update + DeviceManager *deviceManager_; + private: bool isSendQueueDirty_; static const int kMaxPktSize = 16384; uchar pktBuf_[kMaxPktSize]; + // When finding a corresponding device for a packet, we need to inspect + // only uptil the L3 header; in the worst case this would be - + // mac (12) + 4 x vlan (16) + ethType (2) + ipv6 (40) = 74 bytes + // let's round it up to 80 bytes + static const int kMaxL3PktSize = 80; + /*! \note StreamBase::id() and index into streamList[] are NOT same! */ QList streamList_; diff --git a/server/device.cpp b/server/device.cpp new file mode 100644 index 0000000..6655af3 --- /dev/null +++ b/server/device.cpp @@ -0,0 +1,1118 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "device.h" + +#include "../common/emulproto.pb.h" +#include "devicemanager.h" +#include "packetbuffer.h" + +#include +#include + +const int kBaseHex = 16; +const quint64 kBcastMac = 0xffffffffffffULL; +const quint16 kEthTypeIp4 = 0x0800; +const quint16 kEthTypeIp6 = 0x86dd; +const int kIp6HdrLen = 40; +const quint8 kIpProtoIcmp6 = 58; + +/* + * NOTE: + * 1. Device Key is (VLANS + MAC) - is assumed to be unique for a device + * 2. Device clients/users (viz. DeviceManager) should take care when + * setting params that change the key, if the key is used elsewhere + * (e.g. in a hash) + */ + +inline quint32 sumUInt128(UInt128 value) +{ + quint8 *arr = value.toArray(); + quint32 sum = 0; + + for (int i = 0; i < 16; i += 2) + sum += qToBigEndian(*((quint16*)(arr + i))); + + return sum; +} + +inline bool isIp6Mcast(UInt128 ip) +{ + return (ip.hi64() >> 56) == 0xff; +} + +Device::Device(DeviceManager *deviceManager) +{ + deviceManager_ = deviceManager; + + for (int i = 0; i < kMaxVlan; i++) + vlan_[i] = 0; + numVlanTags_ = 0; + mac_ = 0; + + hasIp4_ = false; + hasIp6_ = false; + + clearKey(); +} + +void Device::setVlan(int index, quint16 vlan, quint16 tpid) +{ + int ofs; + + if ((index < 0) || (index >= kMaxVlan)) { + qWarning("%s: vlan index %d out of range (0 - %d)", __FUNCTION__, + index, kMaxVlan - 1); + return; + } + + vlan_[index] = (tpid << 16) | vlan; + + ofs = index * sizeof(quint16); + key_[ofs] = vlan >> 8; + key_[ofs+1] = vlan & 0xff; + + if (index >= numVlanTags_) + numVlanTags_ = index + 1; +} + +quint64 Device::mac() +{ + return mac_; +} + +void Device::setMac(quint64 mac) +{ + int ofs = kMaxVlan * sizeof(quint16); + + mac_ = mac & ~(0xffffULL << 48); + memcpy(key_.data() + ofs, (char*)&mac_, sizeof(mac_)); +} + +void Device::setIp4(quint32 address, int prefixLength, quint32 gateway) +{ + ip4_ = address; + ip4PrefixLength_ = prefixLength; + ip4Gateway_ = gateway; + hasIp4_ = true; +} + +void Device::setIp6(UInt128 address, int prefixLength, UInt128 gateway) +{ + ip6_ = address; + ip6PrefixLength_ = prefixLength; + ip6Gateway_ = gateway; + hasIp6_ = true; +} + +void Device::getConfig(OstEmul::Device *deviceConfig) +{ + for (int i = 0; i < numVlanTags_; i++) + deviceConfig->add_vlan(vlan_[i]); + deviceConfig->set_mac(mac_); + + if (hasIp4_) { + deviceConfig->set_ip4(ip4_); + deviceConfig->set_ip4_prefix_length(ip4PrefixLength_); + deviceConfig->set_ip4_default_gateway(ip4Gateway_); + } + + if (hasIp6_) { + deviceConfig->mutable_ip6()->set_hi(ip6_.hi64()); + deviceConfig->mutable_ip6()->set_lo(ip6_.lo64()); + deviceConfig->set_ip6_prefix_length(ip6PrefixLength_); + deviceConfig->mutable_ip6_default_gateway()->set_hi(ip6Gateway_.hi64()); + deviceConfig->mutable_ip6_default_gateway()->set_lo(ip6Gateway_.lo64()); + } +} + +QString Device::config() +{ + QString config; + + for (int i = 0; i < numVlanTags_; i++) { + config.append(i == 0 ? "vlans=" : "|"); + config.append( + (vlan_[i] >> 16) != kVlanTpid ? + QString("0x%1-%2") + .arg(vlan_[i] >> 16, 4, kBaseHex, QChar('0')) + .arg(vlan_[i] & 0xFFFF) : + QString("%1") + .arg(vlan_[i] & 0xFFFF)); + } + + config.append(QString(" mac=%1") + .arg(mac_, 12, kBaseHex, QChar('0'))); + if (hasIp4_) + config.append(QString(" ip4=%1/%2") + .arg(QHostAddress(ip4_).toString()) + .arg(ip4PrefixLength_)); + if (hasIp6_) + config.append(QString(" ip6=%1/%2") + .arg(QHostAddress(ip6_.toArray()).toString()) + .arg(ip6PrefixLength_)); + + return config; +} + +DeviceKey Device::key() +{ + return key_; +} + +void Device::clearKey() +{ + key_.fill(0, kMaxVlan * sizeof(quint16) + sizeof(quint64)); +} + +int Device::encapSize() +{ + // ethernet header + vlans + int size = 14 + 4*numVlanTags_; + + return size; +} + +void Device::encap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type) +{ + int ofs; + quint64 srcMac = mac_; + uchar *p = pktBuf->push(encapSize()); + + if (!p) { + qWarning("%s: failed to push %d bytes [0x%p, 0x%p]", __FUNCTION__, + encapSize(), pktBuf->head(), pktBuf->data()); + goto _exit; + } + + *(quint32*)(p ) = qToBigEndian(quint32(dstMac >> 16)); + *(quint16*)(p + 4) = qToBigEndian(quint16(dstMac & 0xffff)); + *(quint32*)(p + 6) = qToBigEndian(quint32(srcMac >> 16)); + *(quint16*)(p + 10) = qToBigEndian(quint16(srcMac & 0xffff)); + ofs = 12; + for (int i = 0; i < numVlanTags_; i++) { + *(quint32*)(p + ofs) = qToBigEndian(vlan_[i]); + ofs += 4; + } + *(quint16*)(p + ofs) = qToBigEndian(type); + ofs += 2; + + Q_ASSERT(ofs == encapSize()); + +_exit: + return; +} + +// We expect pktBuf to point to EthType on entry +void Device::receivePacket(PacketBuffer *pktBuf) +{ + quint16 ethType = qFromBigEndian(pktBuf->data()); + pktBuf->pull(2); + + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + + switch(ethType) + { + case 0x0806: // ARP + if (hasIp4_) + receiveArp(pktBuf); + break; + + case 0x0800: // IPv4 + if (hasIp4_) + receiveIp4(pktBuf); + break; + + case 0x86dd: // IPv6 + if (hasIp6_) + receiveIp6(pktBuf); + break; + + default: + break; + } + // FIXME: temporary hack till DeviceManager clones pbufs + pktBuf->push(2); +} + +void Device::transmitPacket(PacketBuffer *pktBuf) +{ + deviceManager_->transmitPacket(pktBuf); +} + +void Device::resolveGateway() +{ + if (hasIp4_) + sendArpRequest(ip4Gateway_); + + if (hasIp6_) + sendNeighborSolicit(ip6Gateway_); +} + +void Device::clearNeighbors(Device::NeighborSet set) +{ + QMutableHashIterator arpIter(arpTable_); + QMutableHashIterator ndpIter(ndpTable_); + + switch (set) { + case kAllNeighbors: + arpTable_.clear(); + ndpTable_.clear(); + break; + + case kUnresolvedNeighbors: + while (arpIter.hasNext()) { + arpIter.next(); + if (arpIter.value() == 0) + arpIter.remove(); + } + + while (ndpIter.hasNext()) { + ndpIter.next(); + if (ndpIter.value() == 0) + ndpIter.remove(); + } + break; + default: + Q_ASSERT(false); // Unreachable! + } +} + +// Resolve the Neighbor IP address for this to-be-transmitted pktBuf +// We expect pktBuf to point to EthType on entry +void Device::resolveNeighbor(PacketBuffer *pktBuf) +{ + quint16 ethType = qFromBigEndian(pktBuf->data()); + pktBuf->pull(2); + + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + + switch(ethType) + { + case 0x0800: // IPv4 + if (hasIp4_) + sendArpRequest(pktBuf); + break; + + case 0x86dd: // IPv6 + if (hasIp6_) + sendNeighborSolicit(pktBuf); + break; + + default: + break; + } + // FIXME: temporary hack till DeviceManager clones pbufs + pktBuf->push(2); +} + +// Append this device's neighbors to the list +void Device::getNeighbors(OstEmul::DeviceNeighborList *neighbors) +{ + QList ip4List = arpTable_.keys(); + QList ip6List = ndpTable_.keys(); + QList macList; + + macList = arpTable_.values(); + Q_ASSERT(ip4List.size() == macList.size()); + + for (int i = 0; i < ip4List.size(); i++) { + OstEmul::ArpEntry *arp = neighbors->add_arp(); + arp->set_ip4(ip4List.at(i)); + arp->set_mac(macList.at(i)); + } + + macList = ndpTable_.values(); + Q_ASSERT(ip6List.size() == macList.size()); + + for (int i = 0; i < ip6List.size(); i++) { + OstEmul::NdpEntry *ndp = neighbors->add_ndp(); + ndp->mutable_ip6()->set_hi(ip6List.at(i).hi64()); + ndp->mutable_ip6()->set_lo(ip6List.at(i).lo64()); + ndp->set_mac(macList.at(i)); + } +} + +// Are we the source of the given packet? +// We expect pktBuf to point to EthType on entry +bool Device::isOrigin(const PacketBuffer *pktBuf) +{ + const uchar *pktData = pktBuf->data(); + quint16 ethType = qFromBigEndian(pktData); + + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + pktData += 2; + + // We know only about IP packets - adjust for ethType length (2 bytes) + // when checking that we have a complete IP header + if ((ethType == 0x0800) && hasIp4_) { // IPv4 + int ipHdrLen = (pktData[0] & 0x0F) << 2; + quint32 srcIp; + + if (pktBuf->length() < (ipHdrLen+2)) { + qDebug("incomplete IPv4 header: expected %d, actual %d", + ipHdrLen, pktBuf->length()); + return false; + } + + srcIp = qFromBigEndian(pktData + ipHdrLen - 8); + qDebug("%s: pktSrcIp/selfIp = 0x%x/0x%x", __FUNCTION__, srcIp, ip4_); + return (srcIp == ip4_); + } + else if ((ethType == kEthTypeIp6) && hasIp6_) { // IPv6 + UInt128 srcIp; + if (pktBuf->length() < (kIp6HdrLen+2)) { + qDebug("incomplete IPv6 header: expected %d, actual %d", + kIp6HdrLen, pktBuf->length()-2); + return false; + } + + srcIp = qFromBigEndian(pktData + 8); + qDebug("%s: pktSrcIp6/selfIp6 = %llx-%llx/%llx-%llx", __FUNCTION__, + srcIp.hi64(), srcIp.lo64(), ip6_.hi64(), ip6_.lo64()); + return (srcIp == ip6_); + } + + return false; +} + +// Return the mac address corresponding to the dstIp of the given packet +// We expect pktBuf to point to EthType on entry +quint64 Device::neighborMac(const PacketBuffer *pktBuf) +{ + const uchar *pktData = pktBuf->data(); + quint16 ethType = qFromBigEndian(pktData); + + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + pktData += 2; + + // We know only about IP packets + if ((ethType == 0x0800) && hasIp4_) { // IPv4 + int ipHdrLen = (pktData[0] & 0x0F) << 2; + quint32 dstIp, tgtIp, mask; + + if (pktBuf->length() < ipHdrLen) { + qDebug("incomplete IPv4 header: expected %d, actual %d", + ipHdrLen, pktBuf->length()); + return false; + } + + dstIp = qFromBigEndian(pktData + ipHdrLen - 4); + mask = ~0 << (32 - ip4PrefixLength_); + qDebug("dst %x mask %x self %x", dstIp, mask, ip4_); + tgtIp = ((dstIp & mask) == (ip4_ & mask)) ? dstIp : ip4Gateway_; + + return arpTable_.value(tgtIp); + } + else if ((ethType == kEthTypeIp6) && hasIp6_) { // IPv6 + UInt128 dstIp, tgtIp, mask; + + if (pktBuf->length() < (kIp6HdrLen+2)) { + qDebug("incomplete IPv6 header: expected %d, actual %d", + kIp6HdrLen, pktBuf->length()-2); + return false; + } + + dstIp = qFromBigEndian(pktData + 24); + mask = ~UInt128(0, 0) << (128 - ip6PrefixLength_); + qDebug("dst %s mask %s self %s", + qPrintable(QHostAddress(dstIp.toArray()).toString()), + qPrintable(QHostAddress(mask.toArray()).toString()), + qPrintable(QHostAddress(ip6_.toArray()).toString())); + tgtIp = ((dstIp & mask) == (ip6_ & mask)) ? dstIp : ip6Gateway_; + + return ndpTable_.value(tgtIp); + } + + return false; +} + +// +// Private Methods +// +/* + * --------------------------------------------------------- + * IPv4 related private methods + * --------------------------------------------------------- + */ +void Device::receiveArp(PacketBuffer *pktBuf) +{ + PacketBuffer *rspPkt; + uchar *pktData = pktBuf->data(); + int offset = 0; + quint16 hwType, protoType; + quint8 hwAddrLen, protoAddrLen; + quint16 opCode; + quint64 srcMac, tgtMac; + quint32 srcIp, tgtIp; + + // Extract tgtIp first to check quickly if this packet is for us or not + tgtIp = qFromBigEndian(pktData + 24); + if (tgtIp != ip4_) { + qDebug("tgtIp %s is not me %s", + qPrintable(QHostAddress(tgtIp).toString()), + qPrintable(QHostAddress(ip4_).toString())); + return; + } + + // Extract annd verify ARP packet contents + hwType = qFromBigEndian(pktData + offset); + offset += 2; + if (hwType != 1) // Mac + goto _invalid_exit; + + protoType = qFromBigEndian(pktData + offset); + offset += 2; + if (protoType != 0x0800) // IPv4 + goto _invalid_exit; + + hwAddrLen = pktData[offset]; + offset += 1; + if (hwAddrLen != 6) + goto _invalid_exit; + + protoAddrLen = pktData[offset]; + offset += 1; + if (protoAddrLen != 4) + goto _invalid_exit; + + opCode = qFromBigEndian(pktData + offset); + offset += 2; + + srcMac = qFromBigEndian(pktData + offset); + offset += 4; + srcMac = (srcMac << 16) | qFromBigEndian(pktData + offset); + offset += 2; + + srcIp = qFromBigEndian(pktData + offset); + offset += 4; + + tgtMac = qFromBigEndian(pktData + offset); + offset += 4; + tgtMac = (tgtMac << 16) | qFromBigEndian(pktData + offset); + offset += 2; + + switch (opCode) + { + case 1: // ARP Request + arpTable_.insert(srcIp, srcMac); + + rspPkt = new PacketBuffer; + rspPkt->reserve(encapSize()); + pktData = rspPkt->put(28); + if (pktData) { + // HTYP, PTYP + *(quint32*)(pktData ) = qToBigEndian(quint32(0x00010800)); + // HLEN, PLEN, OPER + *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0x06040002)); + // Source H/W Addr, Proto Addr + *(quint32*)(pktData+ 8) = qToBigEndian(quint32(mac_ >> 16)); + *(quint16*)(pktData+12) = qToBigEndian(quint16(mac_ & 0xffff)); + *(quint32*)(pktData+14) = qToBigEndian(ip4_); + // Target H/W Addr, Proto Addr + *(quint32*)(pktData+18) = qToBigEndian(quint32(srcMac >> 16)); + *(quint16*)(pktData+22) = qToBigEndian(quint16(srcMac & 0xffff)); + *(quint32*)(pktData+24) = qToBigEndian(srcIp); + } + + encap(rspPkt, srcMac, 0x0806); + transmitPacket(rspPkt); + + qDebug("Sent ARP Reply for srcIp/tgtIp=%s/%s", + qPrintable(QHostAddress(srcIp).toString()), + qPrintable(QHostAddress(tgtIp).toString())); + break; + case 2: // ARP Response + arpTable_.insert(srcIp, srcMac); + break; + + default: + break; + } + + return; + +_invalid_exit: + qWarning("Invalid ARP content"); + return; +} + +// Send ARP request for the IPv4 packet in pktBuf +// pktBuf points to start of IP header +void Device::sendArpRequest(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + int ipHdrLen = (pktData[0] & 0x0F) << 2; + quint32 srcIp = ip4_, dstIp, mask, tgtIp; + + if (pktBuf->length() < ipHdrLen) { + qDebug("incomplete IPv4 header: expected %d, actual %d", + ipHdrLen, pktBuf->length()); + return; + } + + dstIp = qFromBigEndian(pktData + ipHdrLen - 4); + + mask = ~0 << (32 - ip4PrefixLength_); + qDebug("dst %x src %x mask %x", dstIp, srcIp, mask); + tgtIp = ((dstIp & mask) == (srcIp & mask)) ? dstIp : ip4Gateway_; + + sendArpRequest(tgtIp); + +} + +void Device::sendArpRequest(quint32 tgtIp) +{ + quint32 srcIp = ip4_; + PacketBuffer *reqPkt; + uchar *pktData; + + // Validate target IP + if (!tgtIp) + return; + + // This function will be called once per unique stream - which + // may all have the same dst IP; even if dst IP are different the + // gateway for the different dst IP may all be same. However, + // we don't want to send duplicate ARP requests, so we check + // if the tgtIP is already in the cache (resolved or unresolved) + // and if so, we don't resend it + if (arpTable_.contains(tgtIp)) + return; + + reqPkt = new PacketBuffer; + reqPkt->reserve(encapSize()); + pktData = reqPkt->put(28); + if (pktData) { + // HTYP, PTYP + *(quint32*)(pktData ) = qToBigEndian(quint32(0x00010800)); + // HLEN, PLEN, OPER + *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0x06040001)); + // Source H/W Addr, Proto Addr + *(quint32*)(pktData+ 8) = qToBigEndian(quint32(mac_ >> 16)); + *(quint16*)(pktData+12) = qToBigEndian(quint16(mac_ & 0xffff)); + *(quint32*)(pktData+14) = qToBigEndian(srcIp); + // Target H/W Addr, Proto Addr + *(quint32*)(pktData+18) = qToBigEndian(quint32(0)); + *(quint16*)(pktData+22) = qToBigEndian(quint16(0)); + *(quint32*)(pktData+24) = qToBigEndian(tgtIp); + } + + encap(reqPkt, kBcastMac, 0x0806); + transmitPacket(reqPkt); + arpTable_.insert(tgtIp, 0); + + qDebug("Sent ARP Request for srcIp/tgtIp=%s/%s", + qPrintable(QHostAddress(srcIp).toString()), + qPrintable(QHostAddress(tgtIp).toString())); +} + +void Device::receiveIp4(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + uchar ipProto; + quint32 dstIp; + + if (pktData[0] != 0x45) { + qDebug("%s: Unsupported IP version or options (%02x) ", __FUNCTION__, + pktData[0]); + goto _invalid_exit; + } + + if (pktBuf->length() < 20) { + qDebug("incomplete IPv4 header: expected 20, actual %d", + pktBuf->length()); + goto _invalid_exit; + } + + // XXX: We don't verify IP Header checksum + + dstIp = qFromBigEndian(pktData + 16); + if (dstIp != ip4_) { + qDebug("%s: dstIp %x is not me (%x)", __FUNCTION__, dstIp, ip4_); + goto _invalid_exit; + } + + ipProto = pktData[9]; + qDebug("%s: ipProto = %d", __FUNCTION__, ipProto); + switch (ipProto) { + case 1: // ICMP + pktBuf->pull(20); + receiveIcmp4(pktBuf); + break; + default: + qWarning("%s: Unsupported ipProto %d", __FUNCTION__, ipProto); + break; + } + +_invalid_exit: + return; +} + +// This function assumes we are replying back to the same IP +// that originally sent us the packet and therefore we can reuse the +// ingress packet for egress; in other words, it assumes the +// original IP header is intact and will just reuse it after +// minimal modifications +void Device::sendIp4Reply(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->push(20); + uchar origTtl = pktData[8]; + uchar ipProto = pktData[9]; + quint32 srcIp, dstIp; + quint32 sum; + + // Swap src/dst IP addresses + dstIp = qFromBigEndian(pktData + 12); // srcIp in original pkt + srcIp = qFromBigEndian(pktData + 16); // dstIp in original pkt + + if (!arpTable_.contains(dstIp)) { + qWarning("%s: mac not found for %s; unable to send IPv4 packet", + __FUNCTION__, qPrintable(QHostAddress(dstIp).toString())); + return; + } + + *(quint32*)(pktData + 12) = qToBigEndian(srcIp); + *(quint32*)(pktData + 16) = qToBigEndian(dstIp); + + // Reset TTL + pktData[8] = 64; + + // Incremental checksum update (RFC 1624 [Eqn.3]) + // HC' = ~(~HC + ~m + m') + sum = quint16(~qFromBigEndian(pktData + 10)); // old cksum + sum += quint16(~quint16(origTtl << 8 | ipProto)); // old value + sum += quint16(pktData[8] << 8 | ipProto); // new value + while(sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + *(quint16*)(pktData + 10) = qToBigEndian(quint16(~sum)); + + encap(pktBuf, arpTable_.value(dstIp), 0x0800); + transmitPacket(pktBuf); +} + +void Device::receiveIcmp4(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + quint32 sum; + + // XXX: We don't verify icmp checksum + + // We handle only ping request + if (pktData[0] != 8) { // Echo Request + qDebug("%s: Ignoring non echo request (%d)", __FUNCTION__, pktData[0]); + return; + } + + pktData[0] = 0; // Echo Reply + + // Incremental checksum update (RFC 1624 [Eqn.3]) + // HC' = ~(~HC + ~m + m') + sum = quint16(~qFromBigEndian(pktData + 2)); // old cksum + sum += quint16(~quint16(8 << 8 | pktData[1])); // old value + sum += quint16(0 << 8 | pktData[1]); // new value + while(sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + *(quint16*)(pktData + 2) = qToBigEndian(quint16(~sum)); + + sendIp4Reply(pktBuf); + qDebug("Sent ICMP Echo Reply"); +} + +/* + * --------------------------------------------------------- + * IPV6 related private methods + * --------------------------------------------------------- + */ + +void Device::receiveIp6(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + uchar ipProto; + UInt128 dstIp; + + if ((pktData[0] & 0xF0) != 0x60) { + qDebug("%s: Unsupported IP version (%02x) ", __FUNCTION__, + pktData[0]); + goto _invalid_exit; + } + + if (pktBuf->length() < kIp6HdrLen) { + qDebug("incomplete IPv6 header: expected %d, actual %d", + kIp6HdrLen, pktBuf->length()); + goto _invalid_exit; + } + + // FIXME: check for specific mcast address(es) instead of any mcast? + dstIp = qFromBigEndian(pktData + 24); + if (!isIp6Mcast(dstIp) && (dstIp != ip6_)) { + qDebug("%s: dstIp %s is not me (%s)", __FUNCTION__, + qPrintable(QHostAddress(dstIp.toArray()).toString()), + qPrintable(QHostAddress(ip6_.toArray()).toString())); + goto _invalid_exit; + } + + ipProto = pktData[6]; + switch (ipProto) { + case kIpProtoIcmp6: + pktBuf->pull(kIp6HdrLen); + receiveIcmp6(pktBuf); + break; + default: + break; + } + +_invalid_exit: + return; +} + +// pktBuf should point to start of IP payload +bool Device::sendIp6(PacketBuffer *pktBuf, UInt128 dstIp, quint8 protocol) +{ + int payloadLen = pktBuf->length(); + uchar *p = pktBuf->push(kIp6HdrLen); + quint64 dstMac = ndpTable_.value(dstIp); + + if (!p) { + qWarning("%s: failed to push %d bytes [0x%p, 0x%p]", __FUNCTION__, + kIp6HdrLen, pktBuf->head(), pktBuf->data()); + goto _error_exit; + } + + // In case of mcast, derive dstMac + if ((dstIp.hi64() >> 56) == 0xff) + dstMac = (quint64(0x3333) << 32) | (dstIp.lo64() & 0xffffffff); + + if (!dstMac) { + qWarning("%s: mac not found for %s; unable to send IPv6 packet", + __FUNCTION__, + qPrintable(QHostAddress(dstIp.toArray()).toString())); + goto _error_exit; + } + + // Ver(4), TrfClass(8), FlowLabel(8) + *(quint32*)(p ) = qToBigEndian(quint32(0x60000000)); + *(quint16*)(p+ 4) = qToBigEndian(quint16(payloadLen)); + p[6] = protocol; + p[7] = 255; // HopLimit + memcpy(p+ 8, ip6_.toArray(), 16); // Source IP + memcpy(p+24, dstIp.toArray(), 16); // Destination IP + + // FIXME: both these functions should return success/failure + encap(pktBuf, dstMac, kEthTypeIp6); + transmitPacket(pktBuf); + + return true; + +_error_exit: + return false; +} + +// This function assumes we are replying back to the same IP +// that originally sent us the packet and therefore we can reuse the +// ingress packet for egress; in other words, it assumes the +// original IP header is intact and will just reuse it after +// minimal modifications +void Device::sendIp6Reply(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->push(kIp6HdrLen); + UInt128 srcIp, dstIp; + + // Swap src/dst IP addresses + dstIp = qFromBigEndian(pktData + 8); // srcIp in original pkt + srcIp = qFromBigEndian(pktData + 24); // dstIp in original pkt + + if (!ndpTable_.contains(dstIp)) { + qWarning("%s: mac not found for %s; unable to send IPv6 packet", + __FUNCTION__, + qPrintable(QHostAddress(dstIp.toArray()).toString())); + return; + } + + memcpy(pktData + 8, srcIp.toArray(), 16); // Source IP + memcpy(pktData + 24, dstIp.toArray(), 16); // Destination IP + + // Reset TTL + pktData[7] = 64; + + encap(pktBuf, ndpTable_.value(dstIp), 0x86dd); + transmitPacket(pktBuf); +} + +void Device::receiveIcmp6(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + quint8 type = pktData[0]; + quint32 sum; + + // XXX: We don't verify icmp checksum + + switch (type) { + case 128: // ICMPv6 Echo Request + pktData[0] = 129; // Echo Reply + + // Incremental checksum update (RFC 1624 [Eqn.3]) + // HC' = ~(~HC + ~m + m') + sum = quint16(~qFromBigEndian(pktData + 2)); // old cksum + sum += quint16(~quint16(128 << 8 | pktData[1])); // old value + sum += quint16(129 << 8 | pktData[1]); // new value + while(sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + *(quint16*)(pktData + 2) = qToBigEndian(quint16(~sum)); + + sendIp6Reply(pktBuf); + qDebug("Sent ICMPv6 Echo Reply"); + break; + + case 135: // Neigh Solicit + case 136: // Neigh Advt + receiveNdp(pktBuf); + break; + default: + break; + } +} + +void Device::receiveNdp(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + quint8 type = pktData[0]; + int len = pktBuf->length(); + int minLen = 24 + (type == 136 ? 8 : 0); // NA should have the Target TLV + + if (len < minLen) { + qDebug("%s: incomplete NS/NA header: expected %d, actual %d", + __FUNCTION__, minLen, pktBuf->length()); + goto _invalid_exit; + } + + switch (type) + { + case 135: { // Neigh Solicit + // TODO: Validation as per RFC 4861 + sendNeighborAdvertisement(pktBuf); + break; + } + case 136: { // Neigh Advt + quint8 flags = pktData[4]; + const quint8 kSFlag = 0x40; + const quint8 kOFlag = 0x20; + UInt128 tgtIp = qFromBigEndian(pktData + 8); + quint64 mac = ndpTable_.value(tgtIp); + + // Update NDP table only for solicited responses + if (!(flags & kSFlag)) + break; + + if ((flags & kOFlag) || (mac == 0)) { + // Check if we have a Target Link-Layer TLV + if ((pktData[24] != 2) || (pktData[25] != 1)) + goto _invalid_exit; + mac = qFromBigEndian(pktData + 26); + mac = (mac << 16) | qFromBigEndian(pktData + 30); + ndpTable_.insert(tgtIp, mac); + } + break; + } + } +_invalid_exit: + return; +} + +// Send NS for the IPv6 packet in pktBuf +// caller is responsible to check that pktBuf originates from this device +// pktBuf should point to start of IP header +void Device::sendNeighborSolicit(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + UInt128 srcIp = ip6_, dstIp, mask, tgtIp; + + if (pktBuf->length() < kIp6HdrLen) { + qDebug("incomplete IPv6 header: expected %d, actual %d", + kIp6HdrLen, pktBuf->length()); + return; + } + + dstIp = qFromBigEndian(pktData + 24); + + mask = ~UInt128(0, 0) << (128 - ip6PrefixLength_); + qDebug("%s: dst %s src %s mask %s", __FUNCTION__, + qPrintable(QHostAddress(dstIp.toArray()).toString()), + qPrintable(QHostAddress(srcIp.toArray()).toString()), + qPrintable(QHostAddress(mask.toArray()).toString())); + tgtIp = ((dstIp & mask) == (srcIp & mask)) ? dstIp : ip6Gateway_; + + sendNeighborSolicit(tgtIp); +} + +void Device::sendNeighborSolicit(UInt128 tgtIp) +{ + UInt128 dstIp, srcIp = ip6_; + PacketBuffer *reqPkt; + uchar *pktData; + + // Validate target IP + if (tgtIp == UInt128(0, 0)) + return; + + // Do we already have a NDP entry (resolved or unresolved)? + // If so, don't resend (see note in sendArpRequest()) + if (ndpTable_.contains(tgtIp)) + return; + + // Form the solicited node address to be used as dstIp + // ff02::1:ffXX:XXXX/104 + dstIp = UInt128((quint64(0xff02) << 48), + (quint64(0x01ff) << 24) | (tgtIp.lo64() & 0xFFFFFF)); + + reqPkt = new PacketBuffer; + reqPkt->reserve(encapSize() + kIp6HdrLen); + pktData = reqPkt->put(32); + if (pktData) { + // Calculate checksum first - + // start with fixed fields in ICMP Header and IPv6 Pseudo Header ... + quint32 sum = 0x8700 + 0x0101 + 32 + kIpProtoIcmp6; + + // then variable fields from ICMP header ... + sum += sumUInt128(tgtIp); + sum += (mac_ >> 32) + ((mac_ >> 16) & 0xffff) + (mac_ & 0xffff); + + // and variable fields from IPv6 pseudo header + sum += sumUInt128(ip6_); + sum += sumUInt128(dstIp); + + while(sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + + // Type, Code + *(quint16*)(pktData ) = qToBigEndian(quint16(0x8700)); + // Checksum + *(quint16*)(pktData+ 2) = qToBigEndian(quint16(~sum)); + // Reserved + *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0)); + // Target IP + memcpy(pktData+ 8, tgtIp.toArray(), 16); + // Source Addr TLV + MacAddr + *(quint16*)(pktData+24) = qToBigEndian(quint16(0x0101)); + *(quint32*)(pktData+26) = qToBigEndian(quint32(mac_ >> 16)); + *(quint16*)(pktData+30) = qToBigEndian(quint16(mac_ & 0xffff)); + } + + if (!sendIp6(reqPkt, dstIp , kIpProtoIcmp6)) + return; + + ndpTable_.insert(tgtIp, 0); + + qDebug("Sent NDP Request for srcIp/tgtIp=%s/%s", + qPrintable(QHostAddress(srcIp.toArray()).toString()), + qPrintable(QHostAddress(tgtIp.toArray()).toString())); +} + +// Send NA for the NS packet in pktBuf +// pktBuf should point to start of ICMPv6 header +void Device::sendNeighborAdvertisement(PacketBuffer *pktBuf) +{ + PacketBuffer *naPkt; + uchar *pktData = pktBuf->data(); + quint16 flags = 0x6000; // solicit = 1; overide = 1 + uchar *ip6Hdr; + UInt128 tgtIp, srcIp; + + tgtIp = qFromBigEndian(pktData + 8); + if (tgtIp != ip6_) { + qDebug("%s: NS tgtIp %s is not us %s", __FUNCTION__, + qPrintable(QHostAddress(tgtIp.toArray()).toString()), + qPrintable(QHostAddress(ip6_.toArray()).toString())); + ip6Hdr = pktBuf->push(kIp6HdrLen); + return; + } + + ip6Hdr = pktBuf->push(kIp6HdrLen); + srcIp = qFromBigEndian(ip6Hdr + 8); + + if (srcIp == UInt128(0, 0)) { + // reset solicit flag + flags &= ~0x4000; + // NA should be sent to All nodes address + srcIp = UInt128(quint64(0xff02) << 48, quint64(1)); + } + else if (pktBuf->length() >= 32) { // have TLVs? + if ((pktData[24] == 0x01) && (pktData[25] == 0x01)) { // Source TLV + quint64 mac; + mac = qFromBigEndian(pktData + 26); + mac = (mac << 16) | qFromBigEndian(pktData + 30); + ndpTable_.insert(srcIp, mac); + } + } + + naPkt = new PacketBuffer; + naPkt->reserve(encapSize() + kIp6HdrLen); + pktData = naPkt->put(32); + if (pktData) { + // Calculate checksum first - + // start with fixed fields in ICMP Header and IPv6 Pseudo Header ... + quint32 sum = (0x8800 + flags + 0x0201) + (32 + kIpProtoIcmp6); + + // then variable fields from ICMP header ... + sum += sumUInt128(tgtIp); + sum += (mac_ >> 32) + ((mac_ >> 16) & 0xffff) + (mac_ & 0xffff); + + // and variable fields from IPv6 pseudo header + sum += sumUInt128(ip6_); + sum += sumUInt128(srcIp); + + while(sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + + // Type, Code + *(quint16*)(pktData ) = qToBigEndian(quint16(0x8800)); + // Checksum + *(quint16*)(pktData+ 2) = qToBigEndian(quint16(~sum)); + // Flags-Reserved + *(quint32*)(pktData+ 4) = qToBigEndian(quint32(flags << 16)); + // Target IP + memcpy(pktData+ 8, tgtIp.toArray(), 16); + // Target Addr TLV + MacAddr + *(quint16*)(pktData+24) = qToBigEndian(quint16(0x0201)); + *(quint32*)(pktData+26) = qToBigEndian(quint32(mac_ >> 16)); + *(quint16*)(pktData+30) = qToBigEndian(quint16(mac_ & 0xffff)); + } + + if (!sendIp6(naPkt, srcIp , kIpProtoIcmp6)) + return; + + qDebug("Sent Neigh Advt to dstIp for tgtIp=%s/%s", + qPrintable(QHostAddress(srcIp.toArray()).toString()), + qPrintable(QHostAddress(tgtIp.toArray()).toString())); +} + +bool operator<(const DeviceKey &a1, const DeviceKey &a2) +{ + int i = 0; + + while (i < a1.size()) { + if (uchar(a1.at(i)) < uchar(a2.at(i))) + return true; + if (uchar(a1.at(i)) > uchar(a2.at(i))) + return false; + i++; + } + + return false; +} diff --git a/server/device.h b/server/device.h new file mode 100644 index 0000000..f77615b --- /dev/null +++ b/server/device.h @@ -0,0 +1,124 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICE_H +#define _DEVICE_H + +#include "../common/emulproto.pb.h" +#include "../common/protocol.pb.h" +#include "../common/uint128.h" + +#include +#include + +class DeviceManager; +class PacketBuffer; + +class DeviceKey: public QByteArray +{ +}; + +class Device +{ +public: + static const quint16 kVlanTpid = 0x8100; + + enum NeighborSet { + kAllNeighbors, + kUnresolvedNeighbors + }; + +public: + Device(DeviceManager *deviceManager); + + void setVlan(int index, quint16 vlan, quint16 tpid = kVlanTpid); + quint64 mac(); + void setMac(quint64 mac); + void setIp4(quint32 address, int prefixLength, quint32 gateway); + void setIp6(UInt128 address, int prefixLength, UInt128 gateway); + void getConfig(OstEmul::Device *deviceConfig); + QString config(); + + DeviceKey key(); + void clearKey(); + + int encapSize(); + void encap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type); + + void receivePacket(PacketBuffer *pktBuf); + void transmitPacket(PacketBuffer *pktBuf); + + void resolveGateway(); + + void clearNeighbors(Device::NeighborSet set); + void resolveNeighbor(PacketBuffer *pktBuf); + void getNeighbors(OstEmul::DeviceNeighborList *neighbors); + + bool isOrigin(const PacketBuffer *pktBuf); + quint64 neighborMac(const PacketBuffer *pktBuf); + +private: // methods + void receiveArp(PacketBuffer *pktBuf); + void sendArpRequest(PacketBuffer *pktBuf); + void sendArpRequest(quint32 tgtIp); + + void receiveIp4(PacketBuffer *pktBuf); + void sendIp4Reply(PacketBuffer *pktBuf); + + void receiveIcmp4(PacketBuffer *pktBuf); + + void receiveIp6(PacketBuffer *pktBuf); + bool sendIp6(PacketBuffer *pktBuf, UInt128 dstIp, quint8 protocol); + void sendIp6Reply(PacketBuffer *pktBuf); + + void receiveIcmp6(PacketBuffer *pktBuf); + + void receiveNdp(PacketBuffer *pktBuf); + void sendNeighborSolicit(PacketBuffer *pktBuf); + void sendNeighborSolicit(UInt128 tgtIp); + void sendNeighborAdvertisement(PacketBuffer *pktBuf); + +private: // data + static const int kMaxVlan = 4; + + DeviceManager *deviceManager_; + + int numVlanTags_; + quint32 vlan_[kMaxVlan]; + quint64 mac_; + + bool hasIp4_; + quint32 ip4_; + int ip4PrefixLength_; + quint32 ip4Gateway_; + + bool hasIp6_; + UInt128 ip6_; + int ip6PrefixLength_; + UInt128 ip6Gateway_; + + DeviceKey key_; + + QHash arpTable_; + QHash ndpTable_; +}; + +bool operator<(const DeviceKey &a1, const DeviceKey &a2); +#endif + diff --git a/server/devicemanager.cpp b/server/devicemanager.cpp new file mode 100644 index 0000000..95a2420 --- /dev/null +++ b/server/devicemanager.cpp @@ -0,0 +1,494 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "devicemanager.h" + +#include "abstractport.h" +#include "device.h" +#include "../common/emulation.h" +#include "packetbuffer.h" + +#include "../common/emulproto.pb.h" + +#include + +#define __STDC_FORMAT_MACROS +#include + +const quint64 kBcastMac = 0xffffffffffffULL; + +inline UInt128 UINT128(OstEmul::Ip6Address x) +{ + return UInt128(x.hi(), x.lo()); +} + +inline bool isMacMcast(quint64 mac) +{ + return (mac >> 40) & 0x01 == 0x01; +} + + +// XXX: Port owning DeviceManager already uses locks, so we don't use any +// locks within DeviceManager to protect deviceGroupList_ et.al. + +DeviceManager::DeviceManager(AbstractPort *parent) +{ + port_ = parent; +} + +DeviceManager::~DeviceManager() +{ + foreach(Device *dev, deviceList_) + delete dev; + + foreach(OstProto::DeviceGroup *devGrp, deviceGroupList_) + delete devGrp; +} + +int DeviceManager::deviceGroupCount() +{ + return deviceGroupList_.size(); +} + +const OstProto::DeviceGroup* DeviceManager::deviceGroupAtIndex(int index) +{ + if ((index < 0) || (index >= deviceGroupCount())) { + qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, + index, deviceGroupCount() - 1); + return NULL; + } + + // Sort List by 'id', get the id at 'index' and then corresponding devGrp + return deviceGroupList_.value(deviceGroupList_.uniqueKeys().value(index)); +} + +const OstProto::DeviceGroup* DeviceManager::deviceGroup(uint deviceGroupId) +{ + return deviceGroupList_.value(deviceGroupId); +} + +bool DeviceManager::addDeviceGroup(uint deviceGroupId) +{ + OstProto::DeviceGroup *deviceGroup; + + if (deviceGroupList_.contains(deviceGroupId)) { + qWarning("%s: deviceGroup id %u already exists", __FUNCTION__, + deviceGroupId); + return false; + } + + deviceGroup = newDeviceGroup(port_->id()); + deviceGroup->mutable_device_group_id()->set_id(deviceGroupId); + deviceGroupList_.insert(deviceGroupId, deviceGroup); + + enumerateDevices(deviceGroup, kAdd); + + // Start emulation when first device is added + if ((deviceCount() == 1) && port_) + port_->startDeviceEmulation(); + + return true; +} + +bool DeviceManager::deleteDeviceGroup(uint deviceGroupId) +{ + OstProto::DeviceGroup *deviceGroup; + if (!deviceGroupList_.contains(deviceGroupId)) { + qWarning("%s: deviceGroup id %u does not exist", __FUNCTION__, + deviceGroupId); + return false; + } + + deviceGroup = deviceGroupList_.take(deviceGroupId); + enumerateDevices(deviceGroup, kDelete); + delete deviceGroup; + + // Stop emulation if no devices remain + if ((deviceCount() == 0) && port_) + port_->stopDeviceEmulation(); + + return true; +} + +bool DeviceManager::modifyDeviceGroup(const OstProto::DeviceGroup *deviceGroup) +{ + quint32 id = deviceGroup->device_group_id().id(); + OstProto::DeviceGroup *myDeviceGroup = deviceGroupList_.value(id); + if (!myDeviceGroup) { + qWarning("%s: deviceGroup id %u does not exist", __FUNCTION__, id); + return false; + } + + enumerateDevices(myDeviceGroup, kDelete); + + myDeviceGroup->CopyFrom(*deviceGroup); + // If mac step is 0, silently override to 1 - otherwise we won't have + // unique DeviceKeys + if (myDeviceGroup->GetExtension(OstEmul::mac).step() == 0) + myDeviceGroup->MutableExtension(OstEmul::mac)->set_step(1); + // Default value for ip6 step should be 1 (not 0) + if (myDeviceGroup->HasExtension(OstEmul::ip6) + && !myDeviceGroup->GetExtension(OstEmul::ip6).has_step()) + myDeviceGroup->MutableExtension(OstEmul::ip6) + ->mutable_step()->set_lo(1); + + enumerateDevices(myDeviceGroup, kAdd); + + return true; +} + +int DeviceManager::deviceCount() +{ + return deviceList_.size(); +} + +void DeviceManager::getDeviceList( + OstProto::PortDeviceList *deviceList) +{ + foreach(Device *device, sortedDeviceList_) { + OstEmul::Device *dev = + deviceList->AddExtension(OstEmul::device); + device->getConfig(dev); + } +} + +void DeviceManager::receivePacket(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + int offset = 0; + Device dk(this); + Device *device; + quint64 dstMac; + quint16 ethType; + quint16 vlan; + int idx = 0; + + // We assume pkt is ethernet + // TODO: extend for other link layer types + + // All frames we are interested in should be at least 32 bytes + if (pktBuf->length() < 32) { + qWarning("short frame of %d bytes, skipping ...", pktBuf->length()); + goto _exit; + } + + // Extract dstMac + dstMac = qFromBigEndian(pktData + offset); + offset += 4; + dstMac = (dstMac << 16) | qFromBigEndian(pktData + offset); + + qDebug("dstMac %012" PRIx64, dstMac); + + // XXX: Treat multicast as bcast + if (isMacMcast(dstMac)) + dstMac = kBcastMac; + + dk.setMac(dstMac); + offset += 2; + + // Skip srcMac - don't care + offset += 6; + +_eth_type: + // Extract EthType + ethType = qFromBigEndian(pktData + offset); + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + + if (tpidList_.contains(ethType)) { + offset += 2; + vlan = qFromBigEndian(pktData + offset); + dk.setVlan(idx++, vlan); + offset += 2; + qDebug("%s: idx: %d vlan %d", __FUNCTION__, idx, vlan); + goto _eth_type; + } + + pktBuf->pull(offset); + + if (dstMac == kBcastMac) { + QList list = bcastList_.values(dk.key()); + // FIXME: We need to clone the pktBuf before passing to each + // device, otherwise only the first device gets the original + // packet - all subsequent ones get the modified packet! + // NOTE: modification may not be in the pkt data buffer but + // in the HDTE pointers - which is bad as well! + foreach(Device *device, list) + device->receivePacket(pktBuf); + goto _exit; + } + + // Is it destined for us? + device = deviceList_.value(dk.key()); + if (!device) { + qDebug("%s: dstMac %012llx is not us", __FUNCTION__, dstMac); + goto _exit; + } + + device->receivePacket(pktBuf); + +_exit: + delete pktBuf; +} + +void DeviceManager::transmitPacket(PacketBuffer *pktBuf) +{ + port_->sendEmulationPacket(pktBuf); +} + +void DeviceManager::resolveDeviceGateways() +{ + foreach(Device *device, deviceList_) { + device->resolveGateway(); + } +} + +void DeviceManager::clearDeviceNeighbors(Device::NeighborSet set) +{ + foreach(Device *device, deviceList_) + device->clearNeighbors(set); +} + +void DeviceManager::getDeviceNeighbors( + OstProto::PortNeighborList *neighborList) +{ + int count = 0; + + foreach(Device *device, sortedDeviceList_) { + OstEmul::DeviceNeighborList *neighList = + neighborList->AddExtension(OstEmul::device_neighbor); + neighList->set_device_index(count++); + device->getNeighbors(neighList); + } +} + +void DeviceManager::resolveDeviceNeighbor(PacketBuffer *pktBuf) +{ + Device *device = originDevice(pktBuf); + + if (device) + device->resolveNeighbor(pktBuf); +} + +quint64 DeviceManager::deviceMacAddress(PacketBuffer *pktBuf) +{ + Device *device = originDevice(pktBuf); + + return device ? device->mac() : 0; +} + +quint64 DeviceManager::neighborMacAddress(PacketBuffer *pktBuf) +{ + Device *device = originDevice(pktBuf); + + return device ? device->neighborMac(pktBuf) : 0; +} + +// ------------------------------------ // +// Private Methods +// ------------------------------------ // + +Device* DeviceManager::originDevice(PacketBuffer *pktBuf) +{ + uchar *pktData = pktBuf->data(); + int offset = 12; // start parsing after mac addresses + Device dk(this); + quint16 ethType; + quint16 vlan; + int idx = 0; + + // Do we have any devices at all? + if (!deviceCount()) + return NULL; + + // pktBuf will not have the correct dstMac populated, so use bcastMac + // and search for device by IP + + dk.setMac(kBcastMac); + +_eth_type: + ethType = qFromBigEndian(pktData + offset); + qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); + + if (tpidList_.contains(ethType)) { + offset += 2; + vlan = qFromBigEndian(pktData + offset); + dk.setVlan(idx++, vlan); + offset += 2; + qDebug("%s: idx: %d vlan %d", __FUNCTION__, idx, vlan); + goto _eth_type; + } + + pktBuf->pull(offset); + + foreach(Device *device, bcastList_.values(dk.key())) { + if (device->isOrigin(pktBuf)) + return device; + } + + qDebug("couldn't find origin device for packet"); + return NULL; +} + +void DeviceManager::enumerateDevices( + const OstProto::DeviceGroup *deviceGroup, + Operation oper) +{ + Device dk(this); + OstEmul::VlanEmulation pbVlan = deviceGroup->encap() + .GetExtension(OstEmul::vlan); + int numTags = pbVlan.stack_size(); + int n = 1; + QList vlanCount; + + bool hasIp4 = deviceGroup->HasExtension(OstEmul::ip4); + bool hasIp6 = deviceGroup->HasExtension(OstEmul::ip6); + OstEmul::MacEmulation mac = deviceGroup->GetExtension(OstEmul::mac); + OstEmul::Ip4Emulation ip4 = deviceGroup->GetExtension(OstEmul::ip4); + OstEmul::Ip6Emulation ip6 = deviceGroup->GetExtension(OstEmul::ip6); + + /* + * vlanCount[] stores the number of unique vlans at each tag level + * e.g. for a 3-tag config with 2, 3, 4 vlans at each level respectively + * vlanCount = [24, 12, 4] + * 0 - 0, 0, 0 + * 1 - 0, 0, 1 + * 2 - 0, 0, 2 + * 3 - 0, 0, 3 + * 4 - 0, 1, 0 + * 5 - 0, 1, 1 + * 6 - 0, 1, 2 + * 7 - 0, 1, 3 + * 8 - 0, 2, 0 + * 9 - 0, 2, 1 + * 10 - 0, 2, 2 + * 11 - 0, 2, 3 + * 12 - 1, 0, 0 + * 13 - 1, 0, 1 + * 14 - 1, 0, 2 + * 15 - 1, 0, 3 + * 16 - 1, 1, 0 + * 17 - 1, 1, 1 + * 18 - 1, 1, 2 + * 19 - 1, 1, 3 + * 21 - 1, 2, 0 + * 21 - 1, 2, 1 + * 22 - 1, 2, 2 + * 23 - 1, 2, 3 + * + * Note that vlanCount[0] repesents total-number-of-vlans + * + * Another way to think about this is that at a particular vlan tag + * level, we need to repeat a particular vlan-id as many times as the + * next level's count before we can increment the vlan-id at that level + * + * We use this list to calculate the vlan ids for each tag level for + * all the vlans. + * + * For implementation convenience we append a '1' as the last element + */ + vlanCount.append(n); + for (int i = numTags - 1; i >= 0 ; i--) { + OstEmul::VlanEmulation::Vlan vlan = pbVlan.stack(i); + n *= vlan.count(); + vlanCount.prepend(n); + + // Update TPID list + switch (oper) { + case kAdd: + tpidList_[vlan.tpid()]++; + break; + case kDelete: + tpidList_[vlan.tpid()]--; + if (tpidList_[vlan.tpid()] == 0) + tpidList_.remove(vlan.tpid()); + break; + default: + Q_ASSERT(0); // Unreachable + } + } + + QHash::const_iterator iter = tpidList_.constBegin(); + qDebug("Port %s TPID List:", port_->name()); + while (iter != tpidList_.constEnd()) { + qDebug("tpid: %x (%d)", iter.key(), iter.value()); + iter++; + } + + for (int i = 0; i < vlanCount.at(0); i++) { + for (int j = 0; j < numTags; j++) { + OstEmul::VlanEmulation::Vlan vlan = pbVlan.stack(j); + quint16 vlanAdd = (i/vlanCount.at(j+1) % vlan.count())*vlan.step(); + + dk.setVlan(j, vlan.vlan_tag() + vlanAdd, vlan.tpid()); + } + + for (uint k = 0; k < deviceGroup->device_count(); k++) { + Device *device; + quint64 macAdd = k * mac.step(); + quint32 ip4Add = k * ip4.step(); + UInt128 ip6Add = UINT128(ip6.step()) * k; + + dk.setMac(mac.address() + macAdd); + if (hasIp4) + dk.setIp4(ip4.address() + ip4Add, + ip4.prefix_length(), + ip4.default_gateway()); + if (hasIp6) + dk.setIp6(UINT128(ip6.address()) + ip6Add, + ip6.prefix_length(), + UINT128(ip6.default_gateway())); + + switch (oper) { + case kAdd: + if (deviceList_.contains(dk.key())) { + qWarning("%s: error adding device %s (EEXIST)", + __FUNCTION__, qPrintable(dk.config())); + break; + } + device = new Device(this); + *device = dk; + deviceList_.insert(dk.key(), device); + sortedDeviceList_.insert(dk.key(), device); + + dk.setMac(kBcastMac); + bcastList_.insert(dk.key(), device); + qDebug("enumerate(add): %s", qPrintable(device->config())); + break; + + case kDelete: + device = deviceList_.take(dk.key()); + if (!device) { + qWarning("%s: error deleting device %s (NOTFOUND)", + __FUNCTION__, qPrintable(dk.config())); + break; + } + qDebug("enumerate(del): %s", qPrintable(device->config())); + delete device; + sortedDeviceList_.take(dk.key()); // already freed above + + dk.setMac(kBcastMac); + bcastList_.take(dk.key()); // device already freed above + break; + + default: + Q_ASSERT(0); // Unreachable + } + } // foreach device + } // foreach vlan +} diff --git a/server/devicemanager.h b/server/devicemanager.h new file mode 100644 index 0000000..92d4cd0 --- /dev/null +++ b/server/devicemanager.h @@ -0,0 +1,82 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _DEVICE_MANAGER_H +#define _DEVICE_MANAGER_H + +#include "device.h" + +#include +#include +#include +#include + +class AbstractPort; +class PacketBuffer; +namespace OstProto { + class DeviceGroup; +}; + +class DeviceManager +{ +public: + DeviceManager(AbstractPort *parent = 0); + ~DeviceManager(); + + int deviceGroupCount(); + const OstProto::DeviceGroup* deviceGroupAtIndex(int index); + const OstProto::DeviceGroup* deviceGroup(uint deviceGroupId); + + bool addDeviceGroup(uint deviceGroupId); + bool deleteDeviceGroup(uint deviceGroupId); + bool modifyDeviceGroup(const OstProto::DeviceGroup *deviceGroup); + + int deviceCount(); + void getDeviceList(OstProto::PortDeviceList *deviceList); + + void receivePacket(PacketBuffer *pktBuf); + void transmitPacket(PacketBuffer *pktBuf); + + void resolveDeviceGateways(); + + void clearDeviceNeighbors(Device::NeighborSet set = Device::kAllNeighbors); + void resolveDeviceNeighbor(PacketBuffer *pktBuf); + void getDeviceNeighbors(OstProto::PortNeighborList *neighborList); + + quint64 deviceMacAddress(PacketBuffer *pktBuf); + quint64 neighborMacAddress(PacketBuffer *pktBuf); + +private: + enum Operation { kAdd, kDelete }; + + Device* originDevice(PacketBuffer *pktBuf); + void enumerateDevices( + const OstProto::DeviceGroup *deviceGroup, + Operation oper); + + AbstractPort *port_; + QHash deviceGroupList_; + QHash deviceList_; // fast access to devices + QMap sortedDeviceList_; // sorted access to devices + QMultiHash bcastList_; + QHash tpidList_; // Key: TPID, Value: RefCount +}; + +#endif + diff --git a/server/drone.cpp b/server/drone.cpp index 8f8b632..1bdba20 100644 --- a/server/drone.cpp +++ b/server/drone.cpp @@ -69,3 +69,8 @@ bool Drone::init() return true; } + +MyService* Drone::rpcService() +{ + return service; +} diff --git a/server/drone.h b/server/drone.h index ab05fe1..a12f1e7 100644 --- a/server/drone.h +++ b/server/drone.h @@ -32,6 +32,7 @@ public: Drone(QObject *parent = 0); ~Drone(); bool init(); + MyService* rpcService(); private: RpcServer *rpcServer; diff --git a/server/drone.pro b/server/drone.pro index 5022a5b..42f324e 100644 --- a/server/drone.pro +++ b/server/drone.pro @@ -33,6 +33,8 @@ LIBS += -lprotobuf HEADERS += drone.h \ myservice.h SOURCES += \ + devicemanager.cpp \ + device.cpp \ drone_main.cpp \ drone.cpp \ portmanager.cpp \ @@ -43,6 +45,7 @@ SOURCES += \ winpcapport.cpp SOURCES += myservice.cpp SOURCES += pcapextra.cpp +SOURCES += packetbuffer.cpp QMAKE_DISTCLEAN += object_script.* diff --git a/server/drone_main.cpp b/server/drone_main.cpp index e5c1abd..f80e62d 100644 --- a/server/drone_main.cpp +++ b/server/drone_main.cpp @@ -35,6 +35,7 @@ extern ProtocolManager *OstProtocolManager; extern char *version; extern char *revision; +Drone *drone; QSettings *appSettings; int myport; @@ -47,7 +48,6 @@ int main(int argc, char *argv[]) { int exitCode = 0; QCoreApplication app(argc, argv); - Drone *drone; // TODO: command line options // -v (--version) diff --git a/server/myservice.cpp b/server/myservice.cpp index 0a4f7ca..3d8531c 100644 --- a/server/myservice.cpp +++ b/server/myservice.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2010 Srivats P. +Copyright (C) 2010-2015 Srivats P. This file is part of "Ostinato" @@ -20,6 +20,8 @@ along with this program. If not, see #include "myservice.h" +#include "drone.h" + #if 0 #include #include @@ -31,11 +33,14 @@ along with this program. If not, see #include "../common/streambase.h" #include "../rpc/pbrpccontroller.h" +#include "device.h" +#include "devicemanager.h" #include "portmanager.h" #include +extern Drone *drone; extern char *version; MyService::MyService() @@ -239,7 +244,7 @@ void MyService::addStream(::google::protobuf::RpcController* controller, // Append a new "default" stream - actual contents of the new stream is // expected in a subsequent "modifyStream" request - set the stream id // now itself however!!! - stream = new StreamBase; + stream = new StreamBase(portId); stream->setId(request->stream_id(i).id()); portInfo[portId]->addStream(stream); } @@ -358,6 +363,8 @@ void MyService::startTransmit(::google::protobuf::RpcController* /*controller*/, continue; //! \todo (LOW): partial RPC? portLock[portId]->lockForWrite(); + if (portInfo[portId]->isDirty()) + portInfo[portId]->updatePacketList(); portInfo[portId]->startTransmit(); portLock[portId]->unlock(); } @@ -591,3 +598,378 @@ _invalid_version: controller->SetFailed("invalid version information"); done->Run(); } + +/* + * =================================================================== + * Device Emulation + * =================================================================== + * XXX: Streams and Devices are largely non-overlapping from a RPC + * point of view but they *do* intersect e.g. when a stream is trying to + * find its associated device and info from that device such as src/dst + * mac addresses. For this reason, both set of RPCs share the common + * port level locking + * =================================================================== + */ +void MyService::getDeviceGroupIdList( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::DeviceGroupIdList* response, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + response->mutable_port_id()->set_id(portId); + portLock[portId]->lockForRead(); + for (int i = 0; i < devMgr->deviceGroupCount(); i++) + { + OstProto::DeviceGroupId *dgid; + + dgid = response->add_device_group_id(); + dgid->CopyFrom(devMgr->deviceGroupAtIndex(i)->device_group_id()); + } + portLock[portId]->unlock(); + + done->Run(); + return; + +_invalid_port: + controller->SetFailed("Invalid Port Id"); + done->Run(); +} + +void MyService::getDeviceGroupConfig( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::DeviceGroupConfigList* response, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->port_id().id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + response->mutable_port_id()->set_id(portId); + portLock[portId]->lockForRead(); + for (int i = 0; i < request->device_group_id_size(); i++) + { + const OstProto::DeviceGroup *dg; + + dg = devMgr->deviceGroup(request->device_group_id(i).id()); + if (!dg) + continue; //! \todo(LOW): Partial status of RPC + + response->add_device_group()->CopyFrom(*dg); + } + portLock[portId]->unlock(); + + done->Run(); + return; + +_invalid_port: + controller->SetFailed("invalid portid"); + done->Run(); +} + +void MyService::addDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::Ack* /*response*/, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->port_id().id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + if (portInfo[portId]->isTransmitOn()) + goto _port_busy; + + portLock[portId]->lockForWrite(); + for (int i = 0; i < request->device_group_id_size(); i++) + { + quint32 id = request->device_group_id(i).id(); + const OstProto::DeviceGroup *dg = devMgr->deviceGroup(id); + + // If device group with same id as in request exists already ==> error! + if (dg) + continue; //! \todo (LOW): Partial status of RPC + + devMgr->addDeviceGroup(id); + } + portLock[portId]->unlock(); + + //! \todo (LOW): fill-in response "Ack"???? + + done->Run(); + return; + +_port_busy: + controller->SetFailed("Port Busy"); + goto _exit; + +_invalid_port: + controller->SetFailed("invalid portid"); +_exit: + done->Run(); +} + +void MyService::deleteDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::Ack* /*response*/, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->port_id().id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + if (portInfo[portId]->isTransmitOn()) + goto _port_busy; + + portLock[portId]->lockForWrite(); + for (int i = 0; i < request->device_group_id_size(); i++) + devMgr->deleteDeviceGroup(request->device_group_id(i).id()); + portLock[portId]->unlock(); + + //! \todo (LOW): fill-in response "Ack"???? + + done->Run(); + return; + +_port_busy: + controller->SetFailed("Port Busy"); + goto _exit; +_invalid_port: + controller->SetFailed("invalid portid"); +_exit: + done->Run(); +} + +void MyService::modifyDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupConfigList* request, + ::OstProto::Ack* /*response*/, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->port_id().id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + if (portInfo[portId]->isTransmitOn()) + goto _port_busy; + + portLock[portId]->lockForWrite(); + for (int i = 0; i < request->device_group_size(); i++) + devMgr->modifyDeviceGroup(&request->device_group(i)); + portLock[portId]->unlock(); + + //! \todo(LOW): fill-in response "Ack"???? + + done->Run(); + return; + +_port_busy: + controller->SetFailed("Port Busy"); + goto _exit; +_invalid_port: + controller->SetFailed("invalid portid"); +_exit: + done->Run(); +} + +void MyService::getDeviceList( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::PortDeviceList* response, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + response->mutable_port_id()->set_id(portId); + portLock[portId]->lockForRead(); + devMgr->getDeviceList(response); + portLock[portId]->unlock(); + + done->Run(); + return; + +_invalid_port: + controller->SetFailed("Invalid Port Id"); + done->Run(); +} + +void MyService::resolveDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done) +{ + qDebug("In %s", __PRETTY_FUNCTION__); + + for (int i = 0; i < request->port_id_size(); i++) + { + int portId; + + portId = request->port_id(i).id(); + if ((portId < 0) || (portId >= portInfo.size())) + continue; //! \todo (LOW): partial RPC? + + portLock[portId]->lockForWrite(); + portInfo[portId]->resolveDeviceNeighbors(); + portLock[portId]->unlock(); + } + + //! \todo (LOW): fill-in response "Ack"???? + + done->Run(); +} + +void MyService::clearDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done) +{ + qDebug("In %s", __PRETTY_FUNCTION__); + + for (int i = 0; i < request->port_id_size(); i++) + { + int portId; + + portId = request->port_id(i).id(); + if ((portId < 0) || (portId >= portInfo.size())) + continue; //! \todo (LOW): partial RPC? + + portLock[portId]->lockForWrite(); + portInfo[portId]->clearDeviceNeighbors(); + portLock[portId]->unlock(); + } + + //! \todo (LOW): fill-in response "Ack"???? + + done->Run(); +} + +void MyService::getDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::PortNeighborList* response, + ::google::protobuf::Closure* done) +{ + DeviceManager *devMgr; + int portId; + + qDebug("In %s", __PRETTY_FUNCTION__); + + portId = request->id(); + if ((portId < 0) || (portId >= portInfo.size())) + goto _invalid_port; + + devMgr = portInfo[portId]->deviceManager(); + + response->mutable_port_id()->set_id(portId); + portLock[portId]->lockForRead(); + devMgr->getDeviceNeighbors(response); + portLock[portId]->unlock(); + + done->Run(); + return; + +_invalid_port: + controller->SetFailed("Invalid Port Id"); + done->Run(); +} + +/* + * =================================================================== + * Friends + * TODO: Encap these global functions into a DeviceBroker singleton? + * =================================================================== + */ +quint64 getDeviceMacAddress(int portId, int streamId, int frameIndex) +{ + MyService *service = drone->rpcService(); + DeviceManager *devMgr = NULL; + quint64 mac; + + if (!service) + return 0; + + if ((portId >= 0) && (portId < service->portInfo.size())) + devMgr = service->portInfo[portId]->deviceManager(); + + if (!devMgr || !devMgr->deviceCount()) + return 0; + + service->portLock[portId]->lockForWrite(); + mac = service->portInfo[portId]->deviceMacAddress(streamId, frameIndex); + service->portLock[portId]->unlock(); + + return mac; +} + +quint64 getNeighborMacAddress(int portId, int streamId, int frameIndex) +{ + MyService *service = drone->rpcService(); + DeviceManager *devMgr = NULL; + quint64 mac; + + if (!service) + return 0; + + if ((portId >= 0) && (portId < service->portInfo.size())) + devMgr = service->portInfo[portId]->deviceManager(); + + if (!devMgr || !devMgr->deviceCount()) + return 0; + + service->portLock[portId]->lockForWrite(); + mac = service->portInfo[portId]->neighborMacAddress(streamId, frameIndex); + service->portLock[portId]->unlock(); + + return mac; +} diff --git a/server/myservice.h b/server/myservice.h index ea0cc78..acd8c63 100644 --- a/server/myservice.h +++ b/server/myservice.h @@ -105,6 +105,59 @@ public: ::OstProto::VersionCompatibility* response, ::google::protobuf::Closure* done); + // DeviceGroup and Protocol Emulation + virtual void getDeviceGroupIdList( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::DeviceGroupIdList* response, + ::google::protobuf::Closure* done); + virtual void getDeviceGroupConfig( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::DeviceGroupConfigList* response, + ::google::protobuf::Closure* done); + virtual void addDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done); + virtual void deleteDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done); + virtual void modifyDeviceGroup( + ::google::protobuf::RpcController* controller, + const ::OstProto::DeviceGroupConfigList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done); + + virtual void getDeviceList( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::PortDeviceList* response, + ::google::protobuf::Closure* done); + + virtual void resolveDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done); + virtual void clearDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortIdList* request, + ::OstProto::Ack* response, + ::google::protobuf::Closure* done); + virtual void getDeviceNeighbors( + ::google::protobuf::RpcController* controller, + const ::OstProto::PortId* request, + ::OstProto::PortNeighborList* response, + ::google::protobuf::Closure* done); + + friend quint64 getDeviceMacAddress( + int portId, int streamId, int frameIndex); + friend quint64 getNeighborMacAddress( + int portId, int streamId, int frameIndex); signals: void notification(int notifType, SharedProtobufMessage notifData); diff --git a/server/packetbuffer.cpp b/server/packetbuffer.cpp new file mode 100644 index 0000000..0480de7 --- /dev/null +++ b/server/packetbuffer.cpp @@ -0,0 +1,110 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#include "packetbuffer.h" + +// PacketBuffer with full control +PacketBuffer::PacketBuffer(int size) +{ + if (size == 0) + size = 1600; + + buffer_ = new uchar[size]; + is_own_buffer_ = true; + + head_ = data_ = tail_ = buffer_; + end_ = head_ + size; +} + +// PacketBuffer wrapping already existing const buffer +PacketBuffer::PacketBuffer(const uchar *buffer, int size) +{ + // FIXME: ugly const_cast hack!! + buffer_ = const_cast(buffer); + is_own_buffer_ = false; + + head_ = data_ = buffer_; + tail_ = end_ = buffer_ + size; +} + +PacketBuffer::~PacketBuffer() +{ + if (is_own_buffer_) + delete[] buffer_; +} + +int PacketBuffer::length() const +{ + return tail_ - data_; +} + +uchar* PacketBuffer::head() const +{ + return head_; +} + +uchar* PacketBuffer::data() const +{ + return data_; +} + +uchar* PacketBuffer::tail() const +{ + return tail_; +} + +uchar* PacketBuffer::end() const +{ + return end_; +} + +void PacketBuffer::reserve(int len) +{ + data_ += len; + tail_ += len; +} + +uchar* PacketBuffer::pull(int len) +{ + if ((tail_ - data_) < len) + return NULL; + data_ += len; + + return data_; +} + +uchar* PacketBuffer::push(int len) +{ + if ((data_ - head_) < len) + return NULL; + data_ -= len; + + return data_; +} + +uchar* PacketBuffer::put(int len) +{ + uchar *oldTail = tail_; + + if ((end_ - tail_) < len) + return NULL; + tail_ += len; + + return oldTail; +} diff --git a/server/packetbuffer.h b/server/packetbuffer.h new file mode 100644 index 0000000..5b7fbe7 --- /dev/null +++ b/server/packetbuffer.h @@ -0,0 +1,51 @@ +/* +Copyright (C) 2015 Srivats P. + +This file is part of "Ostinato" + +This is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +#ifndef _PACKET_BUFFER_H +#define _PACKET_BUFFER_H + +#include + +class PacketBuffer +{ +public: + PacketBuffer(int size = 0); + PacketBuffer(const uchar *buffer, int size); + ~PacketBuffer(); + + int length() const; + + uchar* head() const; + uchar* data() const; + uchar* tail() const; + uchar* end() const; + + void reserve(int len); + uchar* pull(int len); + uchar* push(int len); + uchar* put(int len); + +private: + uchar *buffer_; + bool is_own_buffer_; + uchar *head_, *data_, *tail_, *end_; +}; + +#endif + diff --git a/server/pcapport.cpp b/server/pcapport.cpp index d690553..45024a1 100644 --- a/server/pcapport.cpp +++ b/server/pcapport.cpp @@ -19,6 +19,9 @@ along with this program. If not, see #include "pcapport.h" +#include "devicemanager.h" +#include "packetbuffer.h" + #include #ifdef Q_OS_WIN32 @@ -81,6 +84,7 @@ PcapPort::PcapPort(int id, const char *device) monitorTx_ = new PortMonitor(device, kDirectionTx, &stats_); transmitter_ = new PortTransmitter(device); capturer_ = new PortCapturer(device); + emulXcvr_ = new EmulationTransceiver(device, deviceManager_); if (!monitorRx_->handle() || !monitorTx_->handle()) isUsable_ = false; @@ -97,12 +101,8 @@ PcapPort::PcapPort(int id, const char *device) { if (strcmp(device, dev->name) == 0) { -#ifdef Q_OS_WIN32 - data_.set_name(QString("if%1").arg(id).toStdString()); -#else if (dev->name) data_.set_name(dev->name); -#endif if (dev->description) data_.set_description(dev->description); @@ -133,6 +133,7 @@ PcapPort::~PcapPort() if (monitorTx_) monitorTx_->stop(); + delete emulXcvr_; delete capturer_; delete transmitter_; @@ -176,6 +177,26 @@ bool PcapPort::setRateAccuracy(AbstractPort::Accuracy accuracy) return false; } +void PcapPort::startDeviceEmulation() +{ + emulXcvr_->start(); +} + +void PcapPort::stopDeviceEmulation() +{ + emulXcvr_->stop(); +} + +int PcapPort::sendEmulationPacket(PacketBuffer *pktBuf) +{ + return emulXcvr_->transmitPacket(pktBuf); +} + +/* + * ------------------------------------------------------------------- * + * Port Monitor + * ------------------------------------------------------------------- * + */ PcapPort::PortMonitor::PortMonitor(const char *device, Direction direction, AbstractPort::PortStats *stats) { @@ -321,6 +342,11 @@ void PcapPort::PortMonitor::stop() pcap_breakloop(handle()); } +/* + * ------------------------------------------------------------------- * + * Port Transmitter + * ------------------------------------------------------------------- * + */ PcapPort::PortTransmitter::PortTransmitter(const char *device) { char errbuf[PCAP_ERRBUF_SIZE] = ""; @@ -750,6 +776,11 @@ void PcapPort::PortTransmitter::udelay(unsigned long usec) #endif } +/* + * ------------------------------------------------------------------- * + * Port Capturer + * ------------------------------------------------------------------- * + */ PcapPort::PortCapturer::PortCapturer(const char *device) { device_ = QString::fromAscii(device); @@ -884,3 +915,218 @@ QFile* PcapPort::PortCapturer::captureFile() { return &capFile_; } + + +/* + * ------------------------------------------------------------------- * + * Transmit+Receiver for Device/ProtocolEmulation + * ------------------------------------------------------------------- * + */ +PcapPort::EmulationTransceiver::EmulationTransceiver(const char *device, + DeviceManager *deviceManager) +{ + device_ = QString::fromAscii(device); + deviceManager_ = deviceManager; + stop_ = false; + state_ = kNotStarted; + handle_ = NULL; +} + +PcapPort::EmulationTransceiver::~EmulationTransceiver() +{ + stop(); +} + +void PcapPort::EmulationTransceiver::run() +{ + int flags = PCAP_OPENFLAG_PROMISCUOUS; + char errbuf[PCAP_ERRBUF_SIZE] = ""; + struct bpf_program bpf; +#if 0 + const char *capture_filter = + "arp or icmp or icmp6 or " + "(vlan and (arp or icmp or icmp6)) or " + "(vlan and vlan and (arp or icmp or icmp6)) or " + "(vlan and vlan and vlan and (arp or icmp or icmp6)) or " + "(vlan and vlan and vlan and vlan and (arp or icmp or icmp6))"; +/* + Ideally we should use the above filter, but the 'vlan' capture filter + in libpcap is implemented as a kludge. From the pcap-filter man page - + + vlan [vlan_id] + Note that the first vlan keyword encountered in expression changes + the decoding offsets for the remainder of expression on the + assumption that the packet is a VLAN packet. + + The vlan [vlan_id] expression may be used more than once, to filter on + VLAN hierarchies. Each use of that expression increments the filter + offsets by 4. + + See https://ask.wireshark.org/questions/31953/unusual-behavior-with-stacked-vlan-tags-and-capture-filter + + So we use the modified filter expression that works as we intend. If ever + libpcap changes their implementation, this will need to change as well. +*/ +#else + const char *capture_filter = + "arp or icmp or icmp6 or " + "(vlan and (arp or icmp or icmp6)) or " + "(vlan and (arp or icmp or icmp6)) or " + "(vlan and (arp or icmp or icmp6)) or " + "(vlan and (arp or icmp or icmp6))"; +#endif + + const int optimize = 1; + + qDebug("In %s", __PRETTY_FUNCTION__); + +#ifdef Q_OS_WIN32 + flags |= PCAP_OPENFLAG_NOCAPTURE_LOCAL; +#endif + +_retry: +#ifdef Q_OS_WIN32 + // NOCAPTURE_LOCAL needs windows only pcap_open() + handle_ = pcap_open(qPrintable(device_), 65535, + flags, 100 /* ms */, NULL, errbuf); +#else + handle_ = pcap_open_live(qPrintable(device_), 65535, + flags, 100 /* ms */, errbuf); +#endif + + if (handle_ == NULL) + { + if (flags && QString(errbuf).contains("promiscuous")) + { + notify("Unable to set promiscuous mode on <%s> - " + "device emulation will not work", qPrintable(device_)); + goto _exit; + } +#ifdef Q_OS_WIN32 + else if ((flags & PCAP_OPENFLAG_NOCAPTURE_LOCAL) + && QString(errbuf).contains("loopback")) + { + qDebug("Can't set no local capture mode %s", qPrintable(device_)); + flags &= ~PCAP_OPENFLAG_NOCAPTURE_LOCAL; + goto _retry; + } +#endif + else + { + notify("Unable to open <%s> [%s] - device emulation will not work", + qPrintable(device_), errbuf); + goto _exit; + } + } + + // TODO: for now the filter is hardcoded to accept tagged/untagged + // ARP/NDP or ICMPv4/v6; when more protocols are added, we may need + // to derive this filter based on which protocols are configured + // on the devices + if (pcap_compile(handle_, &bpf, capture_filter, optimize, 0) < 0) + { + qWarning("%s: error compiling filter: %s", qPrintable(device_), + pcap_geterr(handle_)); + goto _skip_filter; + } + + if (pcap_setfilter(handle_, &bpf) < 0) + { + qWarning("%s: error setting filter: %s", qPrintable(device_), + pcap_geterr(handle_)); + goto _skip_filter; + } + +_skip_filter: + state_ = kRunning; + while (1) + { + int ret; + struct pcap_pkthdr *hdr; + const uchar *data; + + ret = pcap_next_ex(handle_, &hdr, &data); + switch (ret) + { + case 1: + { + PacketBuffer *pktBuf = new PacketBuffer(data, hdr->caplen); +#if 0 + for (int i = 0; i < 64; i++) { + printf("%02x ", data[i]); + if (i % 16 == 0) + printf("\n"); + } + printf("\n"); +#endif + // XXX: deviceManager should free pktBuf before returning + // from this call; if it needs to process the pkt async + // it should make a copy as the pktBuf's data buffer is + // owned by libpcap which does not guarantee data will + // persist across calls to pcap_next_ex() + deviceManager_->receivePacket(pktBuf); + break; + } + case 0: + // timeout: just go back to the loop + break; + case -1: + qWarning("%s: error reading packet (%d): %s", + __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); + break; + case -2: + default: + qFatal("%s: Unexpected return value %d", __PRETTY_FUNCTION__, + ret); + } + + if (stop_) + { + qDebug("user requested receiver stop\n"); + break; + } + } + pcap_close(handle_); + handle_ = NULL; + stop_ = false; + +_exit: + state_ = kFinished; +} + +void PcapPort::EmulationTransceiver::start() +{ + if (state_ == kRunning) { + qWarning("Receive start requested but is already running!"); + return; + } + + state_ = kNotStarted; + QThread::start(); + + while (state_ == kNotStarted) + QThread::msleep(10); +} + +void PcapPort::EmulationTransceiver::stop() +{ + if (state_ == kRunning) { + stop_ = true; + while (state_ == kRunning) + QThread::msleep(10); + } + else { + qWarning("Receive stop requested but is not running!"); + return; + } +} + +bool PcapPort::EmulationTransceiver::isRunning() +{ + return (state_ == kRunning); +} + +int PcapPort::EmulationTransceiver::transmitPacket(PacketBuffer *pktBuf) +{ + return pcap_sendpacket(handle_, pktBuf->data(), pktBuf->length()); +} diff --git a/server/pcapport.h b/server/pcapport.h index 7776a8e..e0c20cf 100644 --- a/server/pcapport.h +++ b/server/pcapport.h @@ -70,6 +70,10 @@ public: virtual bool isCaptureOn() { return capturer_->isRunning(); } virtual QIODevice* captureData() { return capturer_->captureFile(); } + virtual void startDeviceEmulation(); + virtual void stopDeviceEmulation(); + virtual int sendEmulationPacket(PacketBuffer *pktBuf); + protected: enum Direction { @@ -227,6 +231,32 @@ protected: volatile State state_; }; + class EmulationTransceiver: public QThread + { + public: + EmulationTransceiver(const char *device, DeviceManager *deviceManager); + ~EmulationTransceiver(); + void run(); + void start(); + void stop(); + bool isRunning(); + int transmitPacket(PacketBuffer *pktBuf); + + private: + enum State + { + kNotStarted, + kRunning, + kFinished + }; + + QString device_; + DeviceManager *deviceManager_; + volatile bool stop_; + pcap_t *handle_; + volatile State state_; + }; + PortMonitor *monitorRx_; PortMonitor *monitorTx_; @@ -235,6 +265,7 @@ protected: private: PortTransmitter *transmitter_; PortCapturer *capturer_; + EmulationTransceiver *emulXcvr_; static pcap_if_t *deviceList_; }; diff --git a/test/TODO.md b/test/TODO.md new file mode 100644 index 0000000..c6002b5 --- /dev/null +++ b/test/TODO.md @@ -0,0 +1,46 @@ +# TODO - Test Cases + +## Session Save/Open + * Verify save session prompts before opening Save Dialog about only reserved ports being saved if there are some reserved ports and not prompted at all if no ports are reserved + * Verify each save session triggers the file dialog at the last path used + * Verify saved session file is correct + * All portgroups are saved + * All suitable (wrt reservation) ports are saved + * All port configuration is saved + * For each port - + * All streams are saved with correct contents + * All deviceGroups are saved with correct contents + * OSSN Session file format tests (TODO: expand) + * Verify no file is saved if user clicks 'Cancel' on the progress dialog while saving session file + + * On open session, verify user is prompted before opening the file dialog if there are existing portgroups and not prompted at all if there are no port groups + * Verify each open session triggers the file dialog at the last path used + * Verify open file dialog file filter has `(*.ssn *.*)` + * Verify opening a unsupported format file triggers an error and existing session is not changed + * Verify all existing portgroups are removed before new ones from the file are created and configured + * Verify that only ports in the opened session file are overwritten and other ports are not changed + * Verify that if port in the opened session file was reserved at save time, and the same port is now reserved by someone else, it is not changed and user is informed; if current port reservation is by self, port is overwritten with contents from the session file; all reservations made by open session are with self username, not the username who had reserved the port during save time (in other words, allow session files to be exchanged between users) + * Verify no unnecessary RPCs during open session + * if port has no existing streams, deleteStreams() is not invoked + * if port has no existing deviceGroups, deleteDeviceGroups() is not invoked + * if port config has no change, modifyPort() is not invoked + * if opened port has no streams, addStreams()/modifyStreams() is not invoked + * if opened port has no deviceGroups, addDeviceGroups()/modifyDeviceGroups() is not invoked + * Verify open session is successful + * All streams are restored with correct contents + * All deviceGroups are restored with correct contents + * Port config (TxMode, ExclusiveMode, UserName) is changed, if required + * OSSN Session file format tests (TODO: expand) + * Verify no change in existing port groups if user clicks 'Cancel' on the options dialog + * Verify no change in existing port groups if user clicks 'Cancel' on the progress dialog while opening session file + * Verify all old portgroups are removed before new portgroups from the session file are added + * Verify config of portGroups loaded from the session file are correct + * Verify config of a portGroup is NOT restored to the config saved in session file after a open session - disconnect - connect + +## Streams Save/Open + * Verify save file dialog file filter has all supported types but no `*.*` + * Stream file format tests (TODO: expand) + + * Verify open file dialog file filter has all supported types except `*.py` and `*.*` + * Verify opening a unsupported format file triggers an error and existing streams are not changed + * Stream file format tests (TODO: expand) diff --git a/test/emultest.py b/test/emultest.py new file mode 100644 index 0000000..1124354 --- /dev/null +++ b/test/emultest.py @@ -0,0 +1,1265 @@ +#! /usr/bin/env python + +# standard modules +import ipaddress +import logging +import os +import re +import subprocess +import sys +import time + +import pytest + +from fabric.api import run, env, sudo + +from utils import get_tshark + +sys.path.insert(1, '../binding') +from core import ost_pb, emul, DroneProxy +from rpc import RpcError +from protocols.mac_pb2 import mac, Mac +from protocols.ip4_pb2 import ip4, Ip4 +from protocols.ip6_pb2 import ip6, Ip6 +from protocols.vlan_pb2 import vlan + +use_defaults = True + +tshark = get_tshark(minversion = '1.6') +if tshark is None: + print 'tshark >= 1.6 not found' + sys.exit(1) + +# initialize defaults - drone +host_name = '127.0.0.1' + +# initialize defaults - DUT +env.use_shell = False +env.user = 'tc' +env.password = 'tc' +env.host_string = 'localhost:50022' + +# setup logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# command-line option/arg processing +if len(sys.argv) > 1: + if sys.argv[1] in ('-d', '--use-defaults'): + use_defaults = True + if sys.argv[1] in ('-h', '--help'): + print('%s [OPTION]...' % (sys.argv[0])) + print('Options:') + print(' -d --use-defaults run using default values') + print(' -h --help show this help') + sys.exit(0) + +print('This module uses the following topology -') +print('') +print(' +-------+ +-------+') +print(' | |Tx--->---Rx|-+ |') +print(' | Drone | | v DUT |') +print(' | |Rx---<---Tx|-+ |') +print(' +-------+ +-------+') +print('') +print('Drone has 2 ports connected to DUT. Packets sent on the Tx port') +print('are expected to be forwarded by the DUT and received back on the') +print('Rx port') +print('') + +if not use_defaults: + s = raw_input('Drone\'s Hostname/IP [%s]: ' % (host_name)) + host_name = s or host_name + s = raw_input('DUT\'s Hostname/IP [%s]: ' % (env.host_string)) + env.host_string = s or env.host_string + # FIXME: get inputs for dut rx/tx ports + + +# Convenience class to interwork with OstEmul::Ip6Address() and +# the python ipaddress module +class ip6_address(ipaddress.IPv6Interface): + def __init__(self, addr): + if type(addr) is str: + super(ip6_address, self).__init__(unicode(addr)) + elif type(addr) is int: + super(ip6_address, self).__init__(addr) + else: + super(ip6_address, self).__init__(addr.hi << 64 | addr.lo) + self.ip6 = emul.Ip6Address() + self.ip6.hi = int(self) >> 64 + self.ip6.lo = int(self) & 0xffffffffffffffff + + self.prefixlen = self.network.prefixlen + + # we assume gateway is the lowest IP host address in the network + gateway = self.network.network_address + 1 + self.gateway = emul.Ip6Address() + self.gateway.hi = int(gateway) >> 64 + self.gateway.lo = int(gateway) & 0xffffffffffffffff + +# ================================================================= # +# ----------------------------------------------------------------- # +# FIXTURES +# ----------------------------------------------------------------- # +# ================================================================= # +# ----------------------------------------------------------------- # +# Baseline Configuration for subsequent testcases +# NOTES +# * All test cases will emulate devices on both rx and tx ports +# * Each test case will create traffic streams corresponding to +# the devices to check +# ----------------------------------------------------------------- # + +@pytest.fixture(scope='module') +def drone(request): + drn = DroneProxy(host_name) + + log.info('connecting to drone(%s:%d)' % (drn.hostName(), drn.portNumber())) + drn.connect() + + def fin(): + drn.disconnect() + request.addfinalizer(fin) + + return drn + +@pytest.fixture(scope='module') +def ports(request, drone): + port_id_list = drone.getPortIdList() + port_config_list = drone.getPortConfig(port_id_list) + assert len(port_config_list.port) != 0 + + # print port list and find default tx/rx ports + tx_number = -1 + rx_number = -1 + print port_config_list + print('Port List') + print('---------') + for port in port_config_list.port: + print('%d.%s (%s)' % (port.port_id.id, port.name, port.description)) + # use a vhost port as default tx/rx port + if ('vhost' in port.name or 'sun' in port.description.lower()): + if tx_number < 0: + tx_number = port.port_id.id + elif rx_number < 0: + rx_number = port.port_id.id + if ('eth1' in port.name): + tx_number = port.port_id.id + if ('eth2' in port.name): + rx_number = port.port_id.id + + assert tx_number >= 0 + assert rx_number >= 0 + + print('Using port %d as tx port(s)' % tx_number) + print('Using port %d as rx port(s)' % rx_number) + + ports.tx = ost_pb.PortIdList() + ports.tx.port_id.add().id = tx_number; + + ports.rx = ost_pb.PortIdList() + ports.rx.port_id.add().id = rx_number; + return ports + +@pytest.fixture(scope='module') +def dut(request): + # Enable IP forwarding on the DUT (aka make it a router) + sudo('sysctl -w net.ipv4.ip_forward=1') + sudo('sysctl -w net.ipv6.conf.all.forwarding=1') + +@pytest.fixture(scope='module') +def dut_ports(request): + dut_ports.rx = 'eth1' + dut_ports.tx = 'eth2' + + # delete all configuration on the DUT interfaces + sudo('ip address flush dev ' + dut_ports.rx) + sudo('ip address flush dev ' + dut_ports.tx) + return dut_ports + +@pytest.fixture(scope='module') +def emul_ports(request, drone, ports): + emul_ports = ost_pb.PortIdList() + emul_ports.port_id.add().id = ports.tx.port_id[0].id; + emul_ports.port_id.add().id = ports.rx.port_id[0].id; + return emul_ports + +@pytest.fixture(scope='module') +def dgid_list(request, drone, ports): + # ----------------------------------------------------------------- # + # create emulated device(s) on tx/rx ports - each test case will + # modify and reuse these devices as per its needs + # ----------------------------------------------------------------- # + + # delete existing devices, if any, on tx port + dgid_list.tx = drone.getDeviceGroupIdList(ports.tx.port_id[0]) + drone.deleteDeviceGroup(dgid_list.tx) + + # add a emulated device group on tx port + dgid_list.tx = ost_pb.DeviceGroupIdList() + dgid_list.tx.port_id.CopyFrom(ports.tx.port_id[0]) + dgid_list.tx.device_group_id.add().id = 1 + log.info('adding tx device_group %d' % dgid_list.tx.device_group_id[0].id) + drone.addDeviceGroup(dgid_list.tx) + + # delete existing devices, if any, on rx port + dgid_list.rx = drone.getDeviceGroupIdList(ports.rx.port_id[0]) + drone.deleteDeviceGroup(dgid_list.rx) + + # add a emulated device group on rx port + dgid_list.rx = ost_pb.DeviceGroupIdList() + dgid_list.rx.port_id.CopyFrom(ports.rx.port_id[0]) + dgid_list.rx.device_group_id.add().id = 1 + log.info('adding rx device_group %d' % dgid_list.rx.device_group_id[0].id) + drone.addDeviceGroup(dgid_list.rx) + + def fin(): + dgid_list = drone.getDeviceGroupIdList(ports.tx.port_id[0]) + drone.deleteDeviceGroup(dgid_list) + dgid_list = drone.getDeviceGroupIdList(ports.rx.port_id[0]) + drone.deleteDeviceGroup(dgid_list) + request.addfinalizer(fin) + + return dgid_list + +@pytest.fixture(scope='module') +def stream_clear(request, drone, ports): + # delete existing streams, if any, on tx port + sid_list = drone.getStreamIdList(ports.tx.port_id[0]) + drone.deleteStream(sid_list) + +@pytest.fixture +def dut_ip(request, dut_ports): + sudo('ip address add 10.10.1.1/24 dev ' + dut_ports.rx) + sudo('ip address add 10.10.2.1/24 dev ' + dut_ports.tx) + + sudo('ip -6 address add 1234:1::1/96 dev ' + dut_ports.rx) + sudo('ip -6 address add 1234:2::1/96 dev ' + dut_ports.tx) + + def fin(): + sudo('ip address delete 10.10.1.1/24 dev ' + dut_ports.rx) + sudo('ip address delete 10.10.2.1/24 dev ' + dut_ports.tx) + + sudo('ip -6 address delete 1234:1::1/96 dev ' + dut_ports.rx) + sudo('ip -6 address delete 1234:2::1/96 dev ' + dut_ports.tx) + request.addfinalizer(fin) + +@pytest.fixture +def dut_vlans(request, dut_ports): + class Devices(object): + pass + + def create_vdev(devices, vlancfgs): + # device(s) with ALL vlan tags are what we configure and add to netns; + # those with intermediate no of vlans are created but not configured + if len(vlancfgs) == 0: + assert len(devices.rx) == len(devices.tx) + dut_vlans.vlans = [] + for i in range(len(devices.rx)): + vrf = 'vrf' + str(i+1) + sudo('ip netns add ' + vrf) + sudo('ip netns exec ' + vrf + + ' sysctl -w net.ipv6.conf.all.forwarding=1') + + dev = devices.rx[i] + sudo('ip link set ' + dev + + ' netns ' + vrf) + sudo('ip netns exec ' + vrf + + ' ip addr add 10.1.1.1/24' + + ' dev ' + dev) + sudo('ip netns exec ' + vrf + + ' ip -6 addr add 1234:1::1/96' + + ' dev ' + dev) + sudo('ip netns exec ' + vrf + + ' ip link set ' + dev + ' up') + + dev = devices.tx[i] + sudo('ip link set ' + dev + + ' netns ' + vrf) + sudo('ip netns exec ' + vrf + + ' ip addr add 10.1.2.1/24' + + ' dev ' + dev) + sudo('ip netns exec ' + vrf + + ' ip -6 addr add 1234:2::1/96' + + ' dev ' + dev) + sudo('ip netns exec ' + vrf + + ' ip link set ' + dev + ' up') + + dut_vlans.vlans.append(dev[dev.find('.')+1:]) + return dut_vlans.vlans + vcfg = vlancfgs[0] + new_devs = Devices() + new_devs.tx = [] + new_devs.rx = [] + for dev in devices.rx+devices.tx: + for k in range(vcfg['count']): + vlan_id = vcfg['base'] + k*vcfg.get('step', 1) + if 'tpid' in vcfg and vcfg['tpid'] == 0x88a8: + tpid = '802.1ad' + else: + tpid = '802.1q' + dev_name = dev + '.' + str(vlan_id) + sudo('ip link add link ' + dev + + ' name ' + dev_name + + ' type vlan id ' + str(vlan_id) + + ' proto ' + tpid) + sudo('ip link set ' + dev_name + ' up') + if dev in devices.rx: + new_devs.rx.append(dev_name) + else: + new_devs.tx.append(dev_name) + print (str(len(devices.rx)*2*vcfg['count'])+' devices created') + + create_vdev(new_devs, vlancfgs[1:]) + + def delete_vdev(): + if len(vlancfgs) == 0: + return + + vrf_count = 1 + for vcfg in vlancfgs: + vrf_count = vrf_count * vcfg['count'] + + # deleting netns will also delete the devices inside it + for i in range(vrf_count): + vrf = 'vrf' + str(i+1) + sudo('ip netns delete ' + vrf) + + # if we have only single tagged vlans, then we are done + if len(vlancfgs) == 1: + return + + # if we have 2 or more stacked vlan tags, we need to delete the + # intermediate stacked vlan devices which are in the root namespace; + # deleting first level tag vlan devices will delete vlan devices + # stacked on top of it also + vcfg = vlancfgs[0] + for dev in devices.tx+devices.rx: + for k in range(vcfg['count']): + vdev = dev + '.' + str(vcfg['base']+k) + sudo('ip link delete ' + vdev) + + + vlancfgs = getattr(request.function, 'vlan_cfg') + devices = Devices() + devices.tx = [dut_ports.tx] + devices.rx = [dut_ports.rx] + create_vdev(devices, vlancfgs) + + request.addfinalizer(delete_vdev) + + +# ================================================================= # +# ----------------------------------------------------------------- # +# TEST CASES +# ----------------------------------------------------------------- # +# ================================================================= # + +@pytest.mark.parametrize('dev_cfg', [ + {'ip_ver': [4], 'mac_step': 1, 'ip_step': 1}, + {'ip_ver': [6], 'mac_step': 1, 'ip_step': 1}, + {'ip_ver': [4, 6], 'mac_step': 2, 'ip_step': 5}, +]) +def test_multiEmulDevNoVlan(drone, ports, dut, dut_ports, dut_ip, + stream_clear, emul_ports, dgid_list, dev_cfg): + # ----------------------------------------------------------------- # + # TESTCASE: Emulate multiple IPv4 devices (no vlans) + # DUT + # /.1 \.1 + # / \ + # 10.10.1/24 10.10.2/24 + # 1234::1/96 1234::2/96 + # / \ + # /.101-105 \.101-105 + # Host1(s) Host2(s) + # ----------------------------------------------------------------- # + + num_devs = 5 + has_ip4 = True if 4 in dev_cfg['ip_ver'] else False + has_ip6 = True if 6 in dev_cfg['ip_ver'] else False + mac_step = dev_cfg['mac_step'] + ip_step = dev_cfg['ip_step'] + + # configure the tx device(s) + devgrp_cfg = ost_pb.DeviceGroupConfigList() + devgrp_cfg.port_id.CopyFrom(ports.tx.port_id[0]) + dg = devgrp_cfg.device_group.add() + dg.device_group_id.id = dgid_list.tx.device_group_id[0].id + dg.core.name = "Host1" + dg.device_count = num_devs + dg.Extensions[emul.mac].address = 0x000102030a01 + if (mac_step != 1): + dg.Extensions[emul.mac].step = mac_step + if has_ip4: + ip = dg.Extensions[emul.ip4] + ip.address = 0x0a0a0165 + ip.prefix_length = 24 + if (ip_step != 1): + ip.step = ip_step + ip.default_gateway = 0x0a0a0101 + if has_ip6: + ip = dg.Extensions[emul.ip6] + ip6addr = ip6_address('1234:1::65/96') + ip.address.CopyFrom(ip6addr.ip6) + ip.prefix_length = ip6addr.prefixlen + if (ip_step != 1): + ip.step.CopyFrom(ip6_address(ip_step).ip6) + ip.default_gateway.CopyFrom(ip6addr.gateway) + + drone.modifyDeviceGroup(devgrp_cfg) + + # configure the rx device(s) + devgrp_cfg = ost_pb.DeviceGroupConfigList() + devgrp_cfg.port_id.CopyFrom(ports.rx.port_id[0]) + dg = devgrp_cfg.device_group.add() + dg.device_group_id.id = dgid_list.rx.device_group_id[0].id + dg.core.name = "Host1" + dg.device_count = num_devs + dg.Extensions[emul.mac].address = 0x000102030b01 + if (mac_step != 1): + dg.Extensions[emul.mac].step = mac_step + if has_ip4: + ip = dg.Extensions[emul.ip4] + ip.address = 0x0a0a0265 + ip.prefix_length = 24 + if (ip_step != 1): + ip.step = ip_step + ip.default_gateway = 0x0a0a0201 + if has_ip6: + ip = dg.Extensions[emul.ip6] + ip6addr = ip6_address('1234:2::65/96') + ip.address.CopyFrom(ip6addr.ip6) + ip.prefix_length = ip6addr.prefixlen + if (ip_step != 1): + ip.step.CopyFrom(ip6_address(ip_step).ip6) + ip.default_gateway.CopyFrom(ip6addr.gateway) + + drone.modifyDeviceGroup(devgrp_cfg) + + # add the tx stream(s) - we may need more than one + stream_id = ost_pb.StreamIdList() + stream_id.port_id.CopyFrom(ports.tx.port_id[0]) + for i in range(len(dev_cfg['ip_ver'])): + stream_id.stream_id.add().id = i + log.info('adding tx_stream %d' % stream_id.stream_id[i].id) + + drone.addStream(stream_id) + + # configure the tx stream(s) + stream_cfg = ost_pb.StreamConfigList() + stream_cfg.port_id.CopyFrom(ports.tx.port_id[0]) + for i in range(len(dev_cfg['ip_ver'])): + s = stream_cfg.stream.add() + s.stream_id.id = stream_id.stream_id[i].id + s.core.is_enabled = True + s.core.frame_len = 1024 + s.control.packets_per_sec = 100 + s.control.num_packets = 10 + + # setup stream protocols as mac:eth2:ip:udp:payload + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kMacFieldNumber + p.Extensions[mac].dst_mac_mode = Mac.e_mm_resolve + p.Extensions[mac].src_mac_mode = Mac.e_mm_resolve + + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kEth2FieldNumber + + if dev_cfg['ip_ver'][i] == 4: + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kIp4FieldNumber + ip = None + ip = p.Extensions[ip4] + ip.src_ip = 0x0a0a0165 + if ip_step == 1: + ip.src_ip_mode = Ip4.e_im_inc_host + ip.src_ip_count = num_devs + else: + vf = p.variable_field.add() + vf.type = ost_pb.VariableField.kCounter32 + vf.offset = 12 + vf.mask = 0x000000FF + vf.value = 0x00000065 + vf.step = ip_step + vf.mode = ost_pb.VariableField.kIncrement + vf.count = num_devs + ip.dst_ip = 0x0a0a0265 + if ip_step == 1: + ip.dst_ip_mode = Ip4.e_im_inc_host + ip.dst_ip_count = num_devs + else: + vf = p.variable_field.add() + vf.type = ost_pb.VariableField.kCounter32 + vf.offset = 16 + vf.mask = 0x000000FF + vf.value = 0x00000065 + vf.step = ip_step + vf.mode = ost_pb.VariableField.kIncrement + vf.count = num_devs + elif dev_cfg['ip_ver'][i] == 6: + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kIp6FieldNumber + ip = p.Extensions[ip6] + ip6addr = ip6_address('1234:1::65/96') + ip.src_addr_hi = ip6addr.ip6.hi + ip.src_addr_lo = ip6addr.ip6.lo + if ip_step == 1: + ip.src_addr_mode = Ip6.kIncHost + ip.src_addr_count = num_devs + ip.src_addr_prefix = ip6addr.prefixlen + else: + vf = p.variable_field.add() + vf.type = ost_pb.VariableField.kCounter32 + vf.offset = 20 + vf.mask = 0xFFFFFFFF + vf.value = 0x00000065 + vf.step = ip_step + vf.mode = ost_pb.VariableField.kIncrement + vf.count = num_devs + ip6addr = ip6_address('1234:2::65/96') + ip.dst_addr_hi = ip6addr.ip6.hi + ip.dst_addr_lo = ip6addr.ip6.lo + if ip_step == 1: + ip.dst_addr_mode = Ip6.kIncHost + ip.dst_addr_count = num_devs + ip.dst_addr_prefix = ip6addr.prefixlen + else: + vf = p.variable_field.add() + vf.type = ost_pb.VariableField.kCounter32 + vf.offset = 36 + vf.mask = 0xFFFFFFFF + vf.value = 0x00000065 + vf.step = ip_step + vf.mode = ost_pb.VariableField.kIncrement + vf.count = num_devs + else: + assert False # unreachable + + s.protocol.add().protocol_id.id = ost_pb.Protocol.kUdpFieldNumber + s.protocol.add().protocol_id.id = ost_pb.Protocol.kPayloadFieldNumber + + log.info('configuring tx_stream %d' % stream_id.stream_id[i].id) + + drone.modifyStream(stream_cfg) + + # FIXME(needed?): clear tx/rx stats + log.info('clearing tx/rx stats') + drone.clearStats(ports.tx) + drone.clearStats(ports.rx) + + # clear arp/ndp on DUT + sudo('ip neigh flush all') + arp_cache = run('ip neigh show') + assert re.search('10.10.[1-2].1\d\d.*lladdr', arp_cache) == None + assert re.search('1234:[1-2]::\[\da-f]+.*lladdr', arp_cache) == None + + # wait for interface to do DAD? Otherwise we don't get replies for NS + # FIXME: find alternative to sleep + time.sleep(5) + + # clear ARP/NDP cache + log.info('clearing ARP/NDP cache on tx/rx port') + drone.clearDeviceNeighbors(emul_ports) + + log.info('retrieving ARP/NDP entries on tx port to verify clear') + device_list = drone.getDeviceList(emul_ports.port_id[0]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[0]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on tx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + assert(len(device.arp) == 0) + if has_ip6: + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + assert(len(device.ndp) == 0) + + log.info('retrieving ARP/NDP entries on rx port to verify clear') + device_list = drone.getDeviceList(emul_ports.port_id[1]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[1]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on rx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + assert(len(device.arp) == 0) + if has_ip6: + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + assert(len(device.ndp) == 0) + + # resolve ARP/NDP on tx/rx ports + log.info('resolving Neighbors on tx/rx ports ...') + drone.startCapture(emul_ports) + drone.resolveDeviceNeighbors(emul_ports) + time.sleep(3) + drone.stopCapture(emul_ports) + + # verify ARP/NDP Requests sent out from tx port + buff = drone.getCaptureBuffer(emul_ports.port_id[0]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Tx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Tx capture buffer (filtered)') + for i in range(len(dev_cfg['ip_ver'])): + if dev_cfg['ip_ver'][i] == 4: + filter = '(arp.opcode == 1)' \ + ' && (arp.src.proto_ipv4 == 10.10.1.)' \ + ' && (arp.dst.proto_ipv4 == 10.10.1.1)' \ + ' && !expert.severity' + elif dev_cfg['ip_ver'][i] == 6: + filter = '(icmpv6.type == 135)' \ + ' && (ipv6.src == 1234:1::)' \ + ' && (icmpv6.nd.ns.target_address == 1234:1::1)' \ + ' && !expert.severity' + for j in range(num_devs): + if dev_cfg['ip_ver'][i] == 4: + filter = filter.replace('', str(101+j*ip_step)) + elif dev_cfg['ip_ver'][i] == 6: + filter = filter.replace('', format(0x65+j*ip_step, 'x')) + #print filter + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + + # verify ARP/NDP Requests sent out from rx port + buff = drone.getCaptureBuffer(emul_ports.port_id[1]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Rx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Rx capture buffer (filtered)') + for i in range(len(dev_cfg['ip_ver'])): + if dev_cfg['ip_ver'][i] == 4: + filter = '(arp.opcode == 1)' \ + ' && (arp.src.proto_ipv4 == 10.10.2.)' \ + ' && (arp.dst.proto_ipv4 == 10.10.2.1)' \ + ' && !expert.severity' + elif dev_cfg['ip_ver'][i] == 6: + filter = '(icmpv6.type == 135)' \ + ' && (ipv6.src == 1234:2::)' \ + ' && (icmpv6.nd.ns.target_address == 1234:2::1)' \ + ' && !expert.severity' + for j in range(num_devs): + if dev_cfg['ip_ver'][i] == 4: + filter = filter.replace('', str(101+j*ip_step)) + elif dev_cfg['ip_ver'][i] == 6: + filter = filter.replace('', format(0x65+j*ip_step, 'x')) + print filter + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + + # retrieve and verify ARP/NDP Table on tx/rx ports + log.info('retrieving ARP/NDP entries on tx port') + device_list = drone.getDeviceList(emul_ports.port_id[0]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[0]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on tx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + resolved = False + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + if (arp.ip4 == devcfg.ip4_default_gateway) and (arp.mac): + resolved = True + assert resolved + if has_ip6: + resolved = False + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + if (ndp.ip6 == devcfg.ip6_default_gateway) and (ndp.mac): + resolved = True + assert resolved + + log.info('retrieving ARP/NDP entries on rx port') + device_list = drone.getDeviceList(emul_ports.port_id[1]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[1]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on rx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + resolved = False + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + if (arp.ip4 == devcfg.ip4_default_gateway) and (arp.mac): + resolved = True + assert resolved + if has_ip6: + resolved = False + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + if (ndp.ip6 == devcfg.ip6_default_gateway) and (ndp.mac): + resolved = True + assert resolved + + # ping the tx devices from the DUT + for i in range(num_devs): + if has_ip4: + out = run('ping -c3 10.10.1.'+str(101+i*ip_step), warn_only=True) + assert '100% packet loss' not in out + if has_ip6: + out = run('ping -6 -c3 1234:1::'+format(101+i*ip_step, 'x'), + warn_only=True) + assert '100% packet loss' not in out + + # ping the rx devices from the DUT + for i in range(num_devs): + if has_ip4: + out = run('ping -c3 10.10.2.'+str(101+i*ip_step), warn_only=True) + assert '100% packet loss' not in out + if has_ip6: + out = run('ping -6 -c3 1234:2::'+format(101+i*ip_step, 'x'), + warn_only=True) + assert '100% packet loss' not in out + + # We are all set now - so transmit the stream now + drone.startCapture(ports.rx) + drone.startTransmit(ports.tx) + log.info('waiting for transmit to finish ...') + time.sleep(3) + drone.stopTransmit(ports.tx) + drone.stopCapture(ports.rx) + + buff = drone.getCaptureBuffer(ports.rx.port_id[0]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Rx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Rx capture buffer (filtered)') + for i in range(num_devs): + if has_ip4: + filter = '(ip.src == 10.10.1.' + str(101+i*ip_step) + ')' \ + ' && (ip.dst == 10.10.2.' + str(101+i*ip_step) + ')' \ + ' && (eth.dst == 00:01:02:03:0b:' \ + + format(1+i*mac_step, '02x')+')' + print('filter: %s' % filter) + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == s.control.num_packets/num_devs + if has_ip6: + filter = '(ipv6.src == 1234:1::' \ + + format(101+i*ip_step, 'x') + ')' \ + + ' && (ipv6.dst == 1234:2::' \ + + format(101+i*ip_step, 'x') + ')' \ + + ' && (eth.dst == 00:01:02:03:0b:' \ + + format(1+i*mac_step, '02x') + ')' + print('filter: %s' % filter) + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == s.control.num_packets/num_devs + os.remove('capture.pcap') + + drone.stopTransmit(ports.tx) + run('ip neigh show') + +@pytest.mark.parametrize('vlan_cfg,ip_ver', [ + ([{'base': 11, 'count': 5}], [6]), + + ([{'base': 11, 'count': 2}, + {'base': 21, 'count': 3}], [4]), + + ([{'base': 11, 'count': 2, 'tpid': 0x88a8}, + {'base': 21, 'count': 3}], [4, 6]), + + ([{'base': 11, 'count': 2}, + {'base': 21, 'count': 3, 'step': 2}, + {'base': 31, 'count': 2, 'step': 3}], [6]), + + ([{'base': 11, 'count': 2}, + {'base': 21, 'count': 3}, + {'base': 31, 'count': 2}, + {'base': 1, 'count': 3}], [4]) +]) +def test_multiEmulDevPerVlan(request, drone, ports, dut, dut_ports, + stream_clear, emul_ports, dgid_list, vlan_cfg, ip_ver): + # ----------------------------------------------------------------- # + # TESTCASE: Emulate multiple IPv4 device per multiple single-tag VLANs + # + # +- DUT -+ + # .1/ \.1 + # / \ + # 10.1.1/24 10.1.2/24 + # 1234:1/96 1234:2/96 + # [vlans] [vlans] + # / \ + # /.101-103 \.101-103 + # Host1(s) Host2(s) + # ----------------------------------------------------------------- # + + num_vlans = 1 + for vcfg in vlan_cfg: + num_vlans = num_vlans * vcfg['count'] + test_multiEmulDevPerVlan.vlan_cfg = vlan_cfg + num_devs_per_vlan = 3 + has_ip4 = True if 4 in ip_ver else False + has_ip6 = True if 6 in ip_ver else False + if has_ip4: + tx_ip4_base = 0x0a010165 + rx_ip4_base = 0x0a010265 + if has_ip6: + tx_ip6_base = ip6_address('1234:1::65/96') + rx_ip6_base = ip6_address('1234:2::65/96') + + # configure vlans on the DUT + dut_vlans(request, dut_ports) + + assert len(dut_vlans.vlans) == num_vlans + vlan_filter = [] + for i in range(len(dut_vlans.vlans)): + vlan_filter.append('') + ids = dut_vlans.vlans[i].split('.') + for j in range(len(ids)): + filter = '(frame[:4]==:)' \ + .replace('', str(12+j*4)) \ + .replace('', '{:04x}'.format( + vlan_cfg[j].get('tpid', 0x8100))) \ + .replace('', '{:04x}'.format(int(ids[j]))) + if len(vlan_filter[i]) > 0: + vlan_filter[i] += ' && ' + vlan_filter[i] += filter + print i, vlan_filter[i] + + # configure the tx device(s) + devgrp_cfg = ost_pb.DeviceGroupConfigList() + devgrp_cfg.port_id.CopyFrom(ports.tx.port_id[0]) + dg = devgrp_cfg.device_group.add() + dg.device_group_id.id = dgid_list.tx.device_group_id[0].id + dg.core.name = "Host1" + for vcfg in vlan_cfg: + v = dg.encap.Extensions[emul.vlan].stack.add() + v.vlan_tag = vcfg['base'] + v.count = vcfg['count'] + if 'tpid' in vcfg: + v.tpid = vcfg['tpid'] + if 'step' in vcfg: + v.step = vcfg['step'] + dg.device_count = num_devs_per_vlan + dg.Extensions[emul.mac].address = 0x000102030a01 + if has_ip4: + ip = dg.Extensions[emul.ip4] + ip.address = tx_ip4_base + ip.prefix_length = 24 + ip.default_gateway = (tx_ip4_base & 0xffffff00) | 0x01 + if has_ip6: + ip = dg.Extensions[emul.ip6] + ip.address.CopyFrom(tx_ip6_base.ip6) + ip.prefix_length = tx_ip6_base.prefixlen + ip.default_gateway.CopyFrom(tx_ip6_base.gateway) + + drone.modifyDeviceGroup(devgrp_cfg) + + # configure the rx device(s) + devgrp_cfg = ost_pb.DeviceGroupConfigList() + devgrp_cfg.port_id.CopyFrom(ports.rx.port_id[0]) + dg = devgrp_cfg.device_group.add() + dg.device_group_id.id = dgid_list.rx.device_group_id[0].id + dg.core.name = "Host1" + for vcfg in vlan_cfg: + v = dg.encap.Extensions[emul.vlan].stack.add() + v.vlan_tag = vcfg['base'] + v.count = vcfg['count'] + if 'tpid' in vcfg: + v.tpid = vcfg['tpid'] + if 'step' in vcfg: + v.step = vcfg['step'] + dg.device_count = num_devs_per_vlan + dg.Extensions[emul.mac].address = 0x000102030b01 + if has_ip4: + ip = dg.Extensions[emul.ip4] + ip.address = rx_ip4_base + ip.prefix_length = 24 + ip.default_gateway = (rx_ip4_base & 0xffffff00) | 0x01 + if has_ip6: + ip = dg.Extensions[emul.ip6] + ip.address.CopyFrom(rx_ip6_base.ip6) + ip.prefix_length = rx_ip6_base.prefixlen + ip.default_gateway.CopyFrom(rx_ip6_base.gateway) + + drone.modifyDeviceGroup(devgrp_cfg) + + # add the tx stream(s) - we need more than one stream + stream_id = ost_pb.StreamIdList() + stream_id.port_id.CopyFrom(ports.tx.port_id[0]) + for i in range(num_vlans * len(ip_ver)): + stream_id.stream_id.add().id = i + log.info('adding tx_stream %d' % stream_id.stream_id[i].id) + + drone.addStream(stream_id) + + # configure the tx stream(s) + stream_cfg = ost_pb.StreamConfigList() + stream_cfg.port_id.CopyFrom(ports.tx.port_id[0]) + for i in range(num_vlans): + for j in range(len(ip_ver)): + idx = i*len(ip_ver) + j + s = stream_cfg.stream.add() + s.stream_id.id = stream_id.stream_id[idx].id + s.core.name = 'stream ' + str(s.stream_id.id) + s.core.is_enabled = True + s.core.ordinal = idx + s.core.frame_len = 1024 + s.control.packets_per_sec = 100 + s.control.num_packets = num_devs_per_vlan + + # setup stream protocols as mac:vlan:eth2:ip4:udp:payload + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kMacFieldNumber + p.Extensions[mac].dst_mac_mode = Mac.e_mm_resolve + p.Extensions[mac].src_mac_mode = Mac.e_mm_resolve + + ids = dut_vlans.vlans[i].split('.') + for id, idx in zip(ids, range(len(ids))): + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kVlanFieldNumber + p.Extensions[vlan].vlan_tag = int(id) + if 'tpid' in vlan_cfg[idx]: + p.Extensions[vlan].tpid = vlan_cfg[idx]['tpid'] + p.Extensions[vlan].is_override_tpid = True + + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kEth2FieldNumber + + if ip_ver[j] == 4: + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kIp4FieldNumber + ip = p.Extensions[ip4] + ip.src_ip = tx_ip4_base + ip.src_ip_mode = Ip4.e_im_inc_host + ip.src_ip_count = num_devs_per_vlan + ip.dst_ip = rx_ip4_base + ip.dst_ip_mode = Ip4.e_im_inc_host + ip.dst_ip_count = num_devs_per_vlan + elif ip_ver[j] == 6: + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kIp6FieldNumber + ip = p.Extensions[ip6] + + ip.src_addr_hi = tx_ip6_base.ip6.hi + ip.src_addr_lo = tx_ip6_base.ip6.lo + ip.src_addr_mode = Ip6.kIncHost + ip.src_addr_count = num_devs_per_vlan + ip.src_addr_prefix = tx_ip6_base.prefixlen + + ip.dst_addr_hi = rx_ip6_base.ip6.hi + ip.dst_addr_lo = rx_ip6_base.ip6.lo + ip.dst_addr_mode = Ip6.kIncHost + ip.dst_addr_count = num_devs_per_vlan + ip.dst_addr_prefix = rx_ip6_base.prefixlen + else: + assert false # unreachable + + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kUdpFieldNumber + p = s.protocol.add() + p.protocol_id.id = ost_pb.Protocol.kPayloadFieldNumber + + log.info('configuring tx_stream %d' % stream_id.stream_id[idx].id) + + drone.modifyStream(stream_cfg) + + # clear arp/ndp on DUT + for i in range(num_vlans): + vrf = 'vrf' + str(i+1) + + sudo('ip netns exec ' + vrf + + ' ip neigh flush all') + arp_cache = sudo('ip netns exec ' + vrf + + ' ip neigh show') + assert re.search('10.1.[1-2].20[1-5].*lladdr', arp_cache) == None + assert re.search('1234:[1-2]::\[\da-f]+.*lladdr', arp_cache) == None + + # wait for interface to do DAD? Otherwise we don't get replies for NS + # FIXME: find alternative to sleep + if has_ip6: + time.sleep(5) + + # clear ARP/NDP cache + log.info('clearing ARP/NDP cache on tx/rx port') + drone.clearDeviceNeighbors(emul_ports) + + log.info('retrieving ARP/NDP entries on tx port to verify clear') + device_list = drone.getDeviceList(emul_ports.port_id[0]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[0]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on tx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + assert(len(device.arp) == 0) + if has_ip6: + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + assert(len(device.ndp) == 0) + + log.info('retrieving ARP/NDP entries on rx port to verify clear') + device_list = drone.getDeviceList(emul_ports.port_id[1]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[1]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on rx port') + for devcfg, device in zip(device_config, devices): + if has_ip4: + for arp in device.arp: + print('%s: %s %012x' % + (str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + assert(len(device.arp) == 0) + if has_ip6: + for ndp in device.ndp: + print('%s: %s %012x' % + (str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + assert(len(device.ndp) == 0) + + # resolve ARP/NDP on tx/rx ports + log.info('resolving Neighbors on tx/rx ports ...') + drone.startCapture(emul_ports) + drone.resolveDeviceNeighbors(emul_ports) + time.sleep(3) + drone.stopCapture(emul_ports) + + # verify ARP/NDP Requests sent out from tx port + buff = drone.getCaptureBuffer(emul_ports.port_id[0]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Tx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Tx capture buffer (all pkts - vlans only)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Tfields', '-eframe.number', '-eieee8021ad.id', '-evlan.id']) + print(cap_pkts) + log.info('dumping Tx capture buffer (filtered)') + for i in range(num_vlans): + for j in range(len(ip_ver)): + if ip_ver[j] == 4: + filter = vlan_filter[i] + ' && (arp.opcode == 1)' \ + ' && (arp.src.proto_ipv4 == 10.1.1.)' \ + ' && (arp.dst.proto_ipv4 == 10.1.1.1)' \ + ' && !expert.severity' + elif ip_ver[j] == 6: + filter = vlan_filter[i] + ' && (icmpv6.type == 135)' \ + ' && (ipv6.src == 1234:1::)' \ + ' && (icmpv6.nd.ns.target_address == 1234:1::1)' \ + ' && !expert.severity' + for k in range(num_devs_per_vlan): + if ip_ver[j] == 4: + filter = filter.replace('', str(101+k)) + elif ip_ver[j] == 6: + filter = filter.replace('', format(0x65+k, 'x')) + print filter + cap_pkts = subprocess.check_output([tshark, '-nr', + 'capture.pcap', '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + + # verify ARP/NDP Requests sent out from rx port + buff = drone.getCaptureBuffer(emul_ports.port_id[1]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Rx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Rx capture buffer (all pkts - vlans only)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Tfields', '-eframe.number', '-eieee8021ad.id', '-evlan.id']) + print(cap_pkts) + log.info('dumping Rx capture buffer (filtered)') + for i in range(num_vlans): + for j in range(len(ip_ver)): + if ip_ver[j] == 4: + filter = vlan_filter[i] + ' && (arp.opcode == 1)' \ + ' && (arp.src.proto_ipv4 == 10.1.2.)' \ + ' && (arp.dst.proto_ipv4 == 10.1.2.1)' \ + ' && !expert.severity' + elif ip_ver[j] == 6: + filter = vlan_filter[i] + ' && (icmpv6.type == 135)' \ + ' && (ipv6.src == 1234:2::)' \ + ' && (icmpv6.nd.ns.target_address == 1234:2::1)' \ + ' && !expert.severity' + for k in range(num_devs_per_vlan): + if ip_ver[j] == 4: + filter = filter.replace('', str(101+k)) + elif ip_ver[j] == 6: + filter = filter.replace('', format(0x65+k, 'x')) + print filter + cap_pkts = subprocess.check_output([tshark, '-nr', + 'capture.pcap', '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + + # retrieve and verify ARP/NDP Table on tx/rx ports + log.info('retrieving ARP/NDP entries on tx port') + device_list = drone.getDeviceList(emul_ports.port_id[0]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[0]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on tx port') + for devcfg, device in zip(device_config, devices): + vlans = '' + for v in devcfg.vlan: + vlans += str(v & 0xffff) + ' ' + if has_ip4: + resolved = False + for arp in device.arp: + print('%s%s: %s %012x' % + (vlans, str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + if (arp.ip4 == devcfg.ip4_default_gateway) and (arp.mac): + resolved = True + assert resolved + if has_ip6: + resolved = False + for ndp in device.ndp: + print('%s%s: %s %012x' % + (vlans, str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + if (ndp.ip6 == devcfg.ip6_default_gateway) and (ndp.mac): + resolved = True + assert resolved + + log.info('retrieving ARP entries on rx port') + device_list = drone.getDeviceList(emul_ports.port_id[1]) + device_config = device_list.Extensions[emul.device] + neigh_list = drone.getDeviceNeighbors(emul_ports.port_id[1]) + devices = neigh_list.Extensions[emul.device_neighbor] + log.info('ARP/NDP Table on rx port') + for devcfg, device in zip(device_config, devices): + vlans = '' + for v in devcfg.vlan: + vlans += str(v & 0xffff) + ' ' + if has_ip4: + resolved = False + for arp in device.arp: + print('%s%s: %s %012x' % + (vlans, str(ipaddress.ip_address(devcfg.ip4)), + str(ipaddress.ip_address(arp.ip4)), arp.mac)) + if (arp.ip4 == devcfg.ip4_default_gateway) and (arp.mac): + resolved = True + assert resolved + if has_ip6: + resolved = False + for ndp in device.ndp: + print('%s%s: %s %012x' % + (vlans, str(ip6_address(devcfg.ip6)), + str(ip6_address(ndp.ip6)), ndp.mac)) + if (ndp.ip6 == devcfg.ip6_default_gateway) and (ndp.mac): + resolved = True + assert resolved + + # ping the tx devices from the DUT + for i in range(num_vlans): + vrf = 'vrf' + str(i+1) + for j in range(num_devs_per_vlan): + if has_ip4: + out = sudo('ip netns exec ' + vrf + + ' ping -c3 10.1.1.'+str(101+j), warn_only=True) + assert '100% packet loss' not in out + if has_ip6: + out = sudo('ip netns exec ' + vrf + + ' ping -6 -c3 1234:1::'+format(101+j, 'x'), + warn_only=True) + assert '100% packet loss' not in out + + # ping the rx devices from the DUT + for i in range(num_vlans): + vrf = 'vrf' + str(i+1) + for j in range(num_devs_per_vlan): + if has_ip4: + out = sudo('ip netns exec ' + vrf + + ' ping -c3 10.1.2.'+str(101+j), warn_only=True) + assert '100% packet loss' not in out + if has_ip6: + out = sudo('ip netns exec ' + vrf + + ' ping -6 -c3 1234:2::'+format(101+j, 'x'), + warn_only=True) + assert '100% packet loss' not in out + + # we are all set - send data stream(s) + drone.startCapture(ports.rx) + drone.startTransmit(ports.tx) + log.info('waiting for transmit to finish ...') + time.sleep(5) + drone.stopTransmit(ports.tx) + drone.stopCapture(ports.rx) + + # verify data packets received on rx port + buff = drone.getCaptureBuffer(ports.rx.port_id[0]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Rx capture buffer (all)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap']) + print(cap_pkts) + log.info('dumping Rx capture buffer (all pkts - vlans only)') + cap_pkts = subprocess.check_output([tshark, '-nr', 'capture.pcap', + '-Tfields', '-eframe.number', '-eieee8021ad.id', '-evlan.id']) + print(cap_pkts) + log.info('dumping Rx capture buffer (filtered)') + for i in range(num_vlans): + for j in range(num_devs_per_vlan): + if has_ip4: + filter = vlan_filter[i] + \ + ' && (ip.src == 10.1.1.' + str(101+j) + ')' \ + ' && (ip.dst == 10.1.2.' + str(101+j) + ')' \ + ' && (eth.dst == 00:01:02:03:0b:' + format(1+j, '02x')+')' + print('filter: %s' % filter) + cap_pkts = subprocess.check_output([tshark, '-nr', + 'capture.pcap', '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + if has_ip6: + filter = vlan_filter[i] \ + + ' && (ipv6.src == 1234:1::' + format(101+j, 'x') + ')' \ + + ' && (ipv6.dst == 1234:2::' + format(101+j, 'x') + ')' \ + + ' && (eth.dst == 00:01:02:03:0b:' \ + + format(1+j, '02x') + ')' + print('filter: %s' % filter) + cap_pkts = subprocess.check_output([tshark, '-nr', + 'capture.pcap', '-Y', filter]) + print(cap_pkts) + assert cap_pkts.count('\n') == 1 + os.remove('capture.pcap') + +# +# TODO +# * Verify that device_index in OstEmul.DeviceNeighborList matches the +# correct device in OstEmul.PortDeviceList +# * Verify ARP/NDP resolution in a bridging topology +# * Verify non-default IP prefix-length +# * Verify bi-directional streams +# +import pytest +pytest.main(__file__) + diff --git a/test/harness.py b/test/harness.py index e6c2c92..7eb1b27 100644 --- a/test/harness.py +++ b/test/harness.py @@ -46,6 +46,12 @@ class TestSuite: def passed(self): return passed == total and self.completed +class TestPreRequisiteError(Exception): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return self.msg + def extract_column(text, col): """Given a text table, return items in the specified column as a list""" lines = text.splitlines() diff --git a/test/pktlentest.py b/test/pktlentest.py index 0e49d07..de8808d 100644 --- a/test/pktlentest.py +++ b/test/pktlentest.py @@ -9,6 +9,7 @@ import sys import time from harness import extract_column +from utils import get_tshark sys.path.insert(1, '../binding') from core import ost_pb, DroneProxy @@ -21,10 +22,7 @@ host_name = '127.0.0.1' tx_number = -1 rx_number = -1 -if sys.platform == 'win32': - tshark = r'C:\Program Files\Wireshark\tshark.exe' -else: - tshark = 'tshark' +tshark = get_tshark(minversion='1.2.0') fmt = 'column.format:"Packet#","%m","Time","%t","Source","%uns","Destination","%und","Protocol","%p","Size","%L","Info","%i","Expert","%a"' fmt_col = 7 # col# in fmt for Size/PktLength @@ -32,8 +30,10 @@ fmt_col = 7 # col# in fmt for Size/PktLength # setup protocol number dictionary proto_number = {} proto_number['mac'] = ost_pb.Protocol.kMacFieldNumber +proto_number['vlan'] = ost_pb.Protocol.kVlanFieldNumber proto_number['eth2'] = ost_pb.Protocol.kEth2FieldNumber proto_number['ip4'] = ost_pb.Protocol.kIp4FieldNumber +proto_number['ip6'] = ost_pb.Protocol.kIp6FieldNumber proto_number['udp'] = ost_pb.Protocol.kUdpFieldNumber proto_number['payload'] = ost_pb.Protocol.kPayloadFieldNumber @@ -115,8 +115,7 @@ def ports(request, drone): return ports -protolist=['mac eth2 ip4 udp payload', 'mac eth2 ip4 udp'] -@pytest.fixture(scope='module', params=protolist) +@pytest.fixture(scope='module') def stream(request, drone, ports): global proto_number @@ -136,7 +135,39 @@ def stream(request, drone, ports): s.control.packets_per_sec = 100 s.control.num_packets = 10 - # setup stream protocols as mac:eth2:ip:udp:payload + # XXX: don't setup any stream protocols + + def fin(): + # delete streams + log.info('deleting tx_stream %d' % stream_id.stream_id[0].id) + drone.deleteStream(stream_id) + + request.addfinalizer(fin) + + return stream_cfg + +protolist=['mac eth2 ip4 udp payload', 'mac eth2 ip4 udp'] +@pytest.fixture(scope='module', params=protolist) +def stream_toggle_payload(request, drone, ports): + global proto_number + + # add a stream + stream_id = ost_pb.StreamIdList() + stream_id.port_id.CopyFrom(ports.tx.port_id[0]) + stream_id.stream_id.add().id = 1 + log.info('adding tx_stream %d' % stream_id.stream_id[0].id) + drone.addStream(stream_id) + + # configure the stream + stream_cfg = ost_pb.StreamConfigList() + stream_cfg.port_id.CopyFrom(ports.tx.port_id[0]) + s = stream_cfg.stream.add() + s.stream_id.id = stream_id.stream_id[0].id + s.core.is_enabled = True + s.control.packets_per_sec = 100 + s.control.num_packets = 10 + + # setup stream protocols s.ClearField("protocol") protos = request.param.split() for p in protos: @@ -156,11 +187,12 @@ def stream(request, drone, ports): ost_pb.StreamCore.e_fl_dec, ost_pb.StreamCore.e_fl_random ]) -def test_packet_length(drone, ports, stream, mode): - """ Test random length packets """ +def test_framelen_modes(drone, ports, stream_toggle_payload, mode): + """ Test various framelen modes """ min_pkt_len = 100 max_pkt_len = 1000 + stream = stream_toggle_payload stream.stream[0].core.len_mode = mode stream.stream[0].core.frame_len_min = min_pkt_len stream.stream[0].core.frame_len_max = max_pkt_len @@ -168,6 +200,13 @@ def test_packet_length(drone, ports, stream, mode): log.info('configuring tx_stream %d' % stream.stream[0].stream_id.id) drone.modifyStream(stream) + if stream.stream[0].protocol[-1].protocol_id.id \ + == ost_pb.Protocol.kPayloadFieldNumber: + filter = 'udp && !expert.severity' + else: + filter = 'udp && eth.fcs_bad==1' \ + '&& ip.checksum_bad==0 && udp.checksum_bad==0' + # clear tx/rx stats log.info('clearing tx/rx stats') drone.clearStats(ports.tx) @@ -187,9 +226,11 @@ def test_packet_length(drone, ports, stream, mode): log.info('dumping Rx capture buffer') cap_pkts = subprocess.check_output([tshark, '-n', '-r', 'capture.pcap']) print(cap_pkts) + print(filter) cap_pkts = subprocess.check_output([tshark, '-n', '-r', 'capture.pcap', - '-R', 'udp', '-o', fmt]) + '-Y', filter, '-o', fmt]) print(cap_pkts) + assert cap_pkts.count('\n') == stream.stream[0].control.num_packets result = extract_column(cap_pkts, fmt_col) print(result) diffSum = 0 @@ -217,3 +258,51 @@ def test_packet_length(drone, ports, stream, mode): finally: drone.stopTransmit(ports.tx) +def test_frame_trunc(drone, ports, stream): + """ Test frame is truncated even if the protocol sizes exceed framelen """ + + #stream.stream[0].core.frame_len_min = min_pkt_len + + # setup stream protocols as mac:vlan:eth2:ip6:udp:payload + stream.stream[0].ClearField("protocol") + protos = ['mac', 'vlan', 'eth2', 'ip6', 'udp', 'payload'] + for p in protos: + stream.stream[0].protocol.add().protocol_id.id = proto_number[p] + + # note: unless truncated minimum size of payload protocol is 1 + exp_framelen = min(12+4+2+40+8+1, 60) + + log.info('configuring tx_stream %d' % stream.stream[0].stream_id.id) + drone.modifyStream(stream) + + # clear tx/rx stats + log.info('clearing tx/rx stats') + drone.clearStats(ports.tx) + drone.clearStats(ports.rx) + + try: + drone.startCapture(ports.rx) + drone.startTransmit(ports.tx) + log.info('waiting for transmit to finish ...') + time.sleep(3) + drone.stopTransmit(ports.tx) + drone.stopCapture(ports.rx) + + log.info('getting Rx capture buffer') + buff = drone.getCaptureBuffer(ports.rx.port_id[0]) + drone.saveCaptureBuffer(buff, 'capture.pcap') + log.info('dumping Rx capture buffer') + cap_pkts = subprocess.check_output([tshark, '-n', '-r', 'capture.pcap', + '-Y', 'vlan', '-o', fmt]) + print(cap_pkts) + assert cap_pkts.count('\n') == stream.stream[0].control.num_packets + result = extract_column(cap_pkts, fmt_col) + print(result) + for i in range(len(result)): + assert (int(result[i]) == exp_framelen) + + os.remove('capture.pcap') + except RpcError as e: + raise + finally: + drone.stopTransmit(ports.tx) diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..709b0b7 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,22 @@ +#! /usr/bin/env python +import subprocess +import sys + +#from distutils.spawn import find_executable +from distutils.version import LooseVersion + +def get_tshark(minversion = None): + tshark = 'tshark' + #if sys.platform == 'win32': + # tshark = find_executable(tshark) + if tshark and minversion: + out = subprocess.check_output([tshark, '-v']).split(None, 2) + # we expect output to be of the form 'Tshark ..." + ver = out[1] + if LooseVersion(ver) < LooseVersion(minversion): + tshark = None + + if tshark is not None: + print('%s version %s found' % (tshark, ver)) + return tshark +