From d5b70e2c1cabed93d3c2c2d1de825adfee0c8106 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 27 Oct 2014 13:39:36 -0700 Subject: [PATCH] Add AlmanacBinding, to bind a service to an interface Summary: Ref T5833. Allows you to bind a service (like `db.example.com`) to one or more interfaces (for example, to specify a pool with one read/write host and two read-only hosts). You can't configure which hosts have which properties yet, but you can add all the relevant interfaces to the service. Next diff will start supporting service, binding, and device properties like "is writable", "is active", etc., so that Almanac will be able to express operations like "change which database is writable", "disable writes", "bring a device down", etc. Test Plan: See screenshots. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T5833 Differential Revision: https://secure.phabricator.com/D10745 --- .../autopatches/20141017.almanac.binding.sql | 14 ++ .../autopatches/20141017.almanac.bxaction.sql | 19 +++ src/__phutil_library_map__.php | 23 +++ .../PhabricatorAlmanacApplication.php | 4 + .../AlmanacBindingEditController.php | 128 ++++++++++++++++ .../AlmanacBindingViewController.php | 122 +++++++++++++++ .../AlmanacServiceViewController.php | 50 ++++++ .../almanac/editor/AlmanacBindingEditor.php | 139 +++++++++++++++++ .../almanac/phid/AlmanacBindingPHIDType.php | 38 +++++ .../almanac/phid/AlmanacDevicePHIDType.php | 1 + .../almanac/phid/AlmanacInterfacePHIDType.php | 14 +- .../almanac/phid/AlmanacNetworkPHIDType.php | 1 + .../almanac/phid/AlmanacServicePHIDType.php | 1 + .../almanac/query/AlmanacBindingQuery.php | 143 ++++++++++++++++++ .../query/AlmanacBindingTransactionQuery.php | 10 ++ .../almanac/query/AlmanacDeviceQuery.php | 13 ++ .../almanac/storage/AlmanacBinding.php | 112 ++++++++++++++ .../storage/AlmanacBindingTransaction.php | 65 ++++++++ .../almanac/storage/AlmanacInterface.php | 8 +- .../typeahead/AlmanacInterfaceDatasource.php | 51 +++++++ .../almanac/view/AlmanacBindingTableView.php | 86 +++++++++++ 21 files changed, 1039 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20141017.almanac.binding.sql create mode 100644 resources/sql/autopatches/20141017.almanac.bxaction.sql create mode 100644 src/applications/almanac/controller/AlmanacBindingEditController.php create mode 100644 src/applications/almanac/controller/AlmanacBindingViewController.php create mode 100644 src/applications/almanac/editor/AlmanacBindingEditor.php create mode 100644 src/applications/almanac/phid/AlmanacBindingPHIDType.php create mode 100644 src/applications/almanac/query/AlmanacBindingQuery.php create mode 100644 src/applications/almanac/query/AlmanacBindingTransactionQuery.php create mode 100644 src/applications/almanac/storage/AlmanacBinding.php create mode 100644 src/applications/almanac/storage/AlmanacBindingTransaction.php create mode 100644 src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php create mode 100644 src/applications/almanac/view/AlmanacBindingTableView.php diff --git a/resources/sql/autopatches/20141017.almanac.binding.sql b/resources/sql/autopatches/20141017.almanac.binding.sql new file mode 100644 index 0000000000..da02069b43 --- /dev/null +++ b/resources/sql/autopatches/20141017.almanac.binding.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_binding ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + servicePHID VARBINARY(64) NOT NULL, + devicePHID VARBINARY(64) NOT NULL, + interfacePHID VARBINARY(64) NOT NULL, + mailKey BINARY(20) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_service` (servicePHID, interfacePHID), + KEY `key_device` (devicePHID), + KEY `key_interface` (interfacePHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/autopatches/20141017.almanac.bxaction.sql b/resources/sql/autopatches/20141017.almanac.bxaction.sql new file mode 100644 index 0000000000..12cd214e4a --- /dev/null +++ b/resources/sql/autopatches/20141017.almanac.bxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_bindingtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) COLLATE utf8_bin NOT NULL, + authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL, + oldValue LONGTEXT COLLATE utf8_bin NOT NULL, + newValue LONGTEXT COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT COLLATE utf8_bin NOT NULL, + metadata LONGTEXT COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f921a39557..b8714eaf20 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -10,6 +10,15 @@ phutil_register_library_map(array( '__library_version__' => 2, 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', + 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', + 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', + 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', + 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', + 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', + 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', + 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', + 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', + 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', 'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', @@ -30,6 +39,7 @@ phutil_register_library_map(array( 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', + 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', @@ -2947,6 +2957,18 @@ phutil_register_library_map(array( ), 'xmap' => array( 'AlmanacAddress' => 'Phobject', + 'AlmanacBinding' => array( + 'AlmanacDAO', + 'PhabricatorPolicyInterface', + ), + 'AlmanacBindingEditController' => 'AlmanacServiceController', + 'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', + 'AlmanacBindingQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacBindingTableView' => 'AphrontView', + 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacBindingViewController' => 'AlmanacServiceController', 'AlmanacConduitUtil' => 'Phobject', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', @@ -2973,6 +2995,7 @@ phutil_register_library_map(array( 'AlmanacDAO', 'PhabricatorPolicyInterface', ), + 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index 59d57342f8..625850b37f 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -47,6 +47,10 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { 'interface/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacInterfaceEditController', ), + 'binding/' => array( + 'edit/(?:(?P\d+)/)?' => 'AlmanacBindingEditController', + '(?P\d+)/' => 'AlmanacBindingViewController', + ), 'network/' => array( '(?:query/(?P[^/]+)/)?' => 'AlmanacNetworkListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacNetworkEditController', diff --git a/src/applications/almanac/controller/AlmanacBindingEditController.php b/src/applications/almanac/controller/AlmanacBindingEditController.php new file mode 100644 index 0000000000..35179c833b --- /dev/null +++ b/src/applications/almanac/controller/AlmanacBindingEditController.php @@ -0,0 +1,128 @@ +getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $binding = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$binding) { + return new Aphront404Response(); + } + + $service = $binding->getService(); + $is_new = false; + + $service_uri = $service->getURI(); + $cancel_uri = $binding->getURI(); + $title = pht('Edit Binding'); + $save_button = pht('Save Changes'); + } else { + $service = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getStr('serviceID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + $binding = AlmanacBinding::initializeNewBinding($service); + $is_new = true; + + $service_uri = $service->getURI(); + $cancel_uri = $service_uri; + $title = pht('Create Binding'); + $save_button = pht('Create Binding'); + } + + $v_interface = array(); + if ($binding->getInterfacePHID()) { + $v_interface = array($binding->getInterfacePHID()); + } + $e_interface = true; + + $validation_exception = null; + if ($request->isFormPost()) { + $v_interface = $request->getArr('interfacePHIDs'); + + $type_interface = AlmanacBindingTransaction::TYPE_INTERFACE; + + $xactions = array(); + + $xactions[] = id(new AlmanacBindingTransaction()) + ->setTransactionType($type_interface) + ->setNewValue(head($v_interface)); + + $editor = id(new AlmanacBindingEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $editor->applyTransactions($binding, $xactions); + + $binding_uri = $binding->getURI(); + return id(new AphrontRedirectResponse())->setURI($binding_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + $e_interface = $ex->getShortMessage($type_interface); + } + } + + $interface_handles = array(); + if ($v_interface) { + $interface_handles = $this->loadViewerHandles($v_interface); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('interfacePHIDs') + ->setLabel('Interface') + ->setLimit(1) + ->setDatasource(new AlmanacInterfaceDatasource()) + ->setValue($interface_handles) + ->setError($e_interface)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($cancel_uri) + ->setValue($save_button)); + + $box = id(new PHUIObjectBoxView()) + ->setValidationException($validation_exception) + ->setHeaderText($title) + ->appendChild($form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($service->getName(), $service_uri); + if ($is_new) { + $crumbs->addTextCrumb(pht('Create Binding')); + } else { + $crumbs->addTextCrumb(pht('Edit Binding')); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/almanac/controller/AlmanacBindingViewController.php b/src/applications/almanac/controller/AlmanacBindingViewController.php new file mode 100644 index 0000000000..b7ff42ce03 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacBindingViewController.php @@ -0,0 +1,122 @@ +getViewer(); + + $id = $request->getURIData('id'); + + $binding = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$binding) { + return new Aphront404Response(); + } + + $service = $binding->getService(); + $service_uri = $service->getURI(); + + $title = pht('Binding %s', $binding->getID()); + + $property_list = $this->buildPropertyList($binding); + $action_list = $this->buildActionList($binding); + $property_list->setActionList($action_list); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($title) + ->setPolicyObject($binding); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($property_list); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($service->getName(), $service_uri); + $crumbs->addTextCrumb($title); + + $xactions = id(new AlmanacBindingTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($binding->getPHID())) + ->execute(); + + $xaction_view = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($binding->getPHID()) + ->setTransactions($xactions) + ->setShouldTerminate(true); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $xaction_view, + ), + array( + 'title' => $title, + )); + } + + private function buildPropertyList(AlmanacBinding $binding) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $handles = $this->loadViewerHandles( + array( + $binding->getServicePHID(), + $binding->getDevicePHID(), + $binding->getInterface()->getNetworkPHID(), + )); + + $properties->addProperty( + pht('Service'), + $handles[$binding->getServicePHID()]->renderLink()); + + $properties->addProperty( + pht('Device'), + $handles[$binding->getDevicePHID()]->renderLink()); + + $properties->addProperty( + pht('Network'), + $handles[$binding->getInterface()->getNetworkPHID()]->renderLink()); + + $properties->addProperty( + pht('Interface'), + $binding->getInterface()->renderDisplayAddress()); + + return $properties; + } + + private function buildActionList(AlmanacBinding $binding) { + $viewer = $this->getViewer(); + $id = $binding->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $binding, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Binding')) + ->setHref($this->getApplicationURI("binding/edit/{$id}/")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $actions; + } + +} diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index e6a8365bed..8218e3eafe 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -35,6 +35,8 @@ final class AlmanacServiceViewController ->setHeader($header) ->addPropertyList($property_list); + $bindings = $this->buildBindingList($service); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($service->getName()); @@ -53,6 +55,7 @@ final class AlmanacServiceViewController array( $crumbs, $box, + $bindings, $xaction_view, ), array( @@ -92,4 +95,51 @@ final class AlmanacServiceViewController return $actions; } + private function buildBindingList(AlmanacService $service) { + $viewer = $this->getViewer(); + $id = $service->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $service, + PhabricatorPolicyCapability::CAN_EDIT); + + $bindings = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withServicePHIDs(array($service->getPHID())) + ->execute(); + + $phids = array(); + foreach ($bindings as $binding) { + $phids[] = $binding->getServicePHID(); + $phids[] = $binding->getDevicePHID(); + $phids[] = $binding->getInterface()->getNetworkPHID(); + } + $handles = $this->loadViewerHandles($phids); + + $table = id(new AlmanacBindingTableView()) + ->setNoDataString( + pht('This service has not been bound to any device interfaces yet.')) + ->setUser($viewer) + ->setBindings($bindings) + ->setHandles($handles); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Service Bindings')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($this->getApplicationURI("binding/edit/?serviceID={$id}")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit) + ->setText(pht('Add Binding')) + ->setIcon( + id(new PHUIIconView()) + ->setIconFont('fa-plus'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($table); + } + } diff --git a/src/applications/almanac/editor/AlmanacBindingEditor.php b/src/applications/almanac/editor/AlmanacBindingEditor.php new file mode 100644 index 0000000000..28eb233c6f --- /dev/null +++ b/src/applications/almanac/editor/AlmanacBindingEditor.php @@ -0,0 +1,139 @@ +getTransactionType()) { + case AlmanacBindingTransaction::TYPE_INTERFACE: + return $object->getInterfacePHID(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_INTERFACE: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_INTERFACE: + $interface = id(new AlmanacInterfaceQuery()) + ->setViewer($this->requireActor()) + ->withPHIDs(array($xaction->getNewValue())) + ->executeOne(); + $object->setDevicePHID($interface->getDevicePHID()); + $object->setInterfacePHID($interface->getPHID()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_INTERFACE: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacBindingTransaction::TYPE_INTERFACE: + $missing = $this->validateIsEmptyTextField( + $object->getInterfacePHID(), + $xactions); + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Bindings must specify an interface.'), + nonempty(last($xactions), null)); + $error->setIsMissingFieldError(true); + $errors[] = $error; + } else if ($xactions) { + foreach ($xactions as $xaction) { + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer($this->requireActor()) + ->withPHIDs(array($xaction->getNewValue())) + ->execute(); + if (!$interfaces) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'You can not bind a service to an invalid or restricted '. + 'interface.'), + $xaction); + $errors[] = $error; + } + } + + $final_value = last($xactions)->getNewValue(); + + $binding = id(new AlmanacBindingQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withServicePHIDs(array($object->getServicePHID())) + ->withInterfacePHIDs(array($final_value)) + ->executeOne(); + if ($binding && ($binding->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Already Bound'), + pht( + 'You can not bind a service to the same interface multiple '. + 'times.'), + last($xactions)); + $errors[] = $error; + } + } + break; + } + + return $errors; + } + + + +} diff --git a/src/applications/almanac/phid/AlmanacBindingPHIDType.php b/src/applications/almanac/phid/AlmanacBindingPHIDType.php new file mode 100644 index 0000000000..d8fcb510fb --- /dev/null +++ b/src/applications/almanac/phid/AlmanacBindingPHIDType.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $binding = $objects[$phid]; + + $id = $binding->getID(); + + $handle->setObjectName(pht('Binding %d', $id)); + $handle->setName(pht('Binding %d', $id)); + } + } + +} diff --git a/src/applications/almanac/phid/AlmanacDevicePHIDType.php b/src/applications/almanac/phid/AlmanacDevicePHIDType.php index 56dc8a5d2f..8a1bb36a90 100644 --- a/src/applications/almanac/phid/AlmanacDevicePHIDType.php +++ b/src/applications/almanac/phid/AlmanacDevicePHIDType.php @@ -33,6 +33,7 @@ final class AlmanacDevicePHIDType extends PhabricatorPHIDType { $handle->setObjectName(pht('Device %d', $id)); $handle->setName($name); + $handle->setURI($device->getURI()); } } diff --git a/src/applications/almanac/phid/AlmanacInterfacePHIDType.php b/src/applications/almanac/phid/AlmanacInterfacePHIDType.php index 609a548c65..67f9b1664a 100644 --- a/src/applications/almanac/phid/AlmanacInterfacePHIDType.php +++ b/src/applications/almanac/phid/AlmanacInterfacePHIDType.php @@ -30,8 +30,20 @@ final class AlmanacInterfacePHIDType extends PhabricatorPHIDType { $id = $interface->getID(); + $device_name = $interface->getDevice()->getName(); + $address = $interface->getAddress(); + $port = $interface->getPort(); + $network = $interface->getNetwork()->getName(); + + $name = pht( + '%s:%s (%s on %s)', + $device_name, + $port, + $address, + $network); + $handle->setObjectName(pht('Interface %d', $id)); - $handle->setName(pht('Interface %d', $id)); + $handle->setName($name); } } diff --git a/src/applications/almanac/phid/AlmanacNetworkPHIDType.php b/src/applications/almanac/phid/AlmanacNetworkPHIDType.php index 8a4be09cd3..e27efa5cd8 100644 --- a/src/applications/almanac/phid/AlmanacNetworkPHIDType.php +++ b/src/applications/almanac/phid/AlmanacNetworkPHIDType.php @@ -33,6 +33,7 @@ final class AlmanacNetworkPHIDType extends PhabricatorPHIDType { $handle->setObjectName(pht('Network %d', $id)); $handle->setName($name); + $handle->setURI($network->getURI()); } } diff --git a/src/applications/almanac/phid/AlmanacServicePHIDType.php b/src/applications/almanac/phid/AlmanacServicePHIDType.php index 19d6634c6b..c64e089ce6 100644 --- a/src/applications/almanac/phid/AlmanacServicePHIDType.php +++ b/src/applications/almanac/phid/AlmanacServicePHIDType.php @@ -33,6 +33,7 @@ final class AlmanacServicePHIDType extends PhabricatorPHIDType { $handle->setObjectName(pht('Service %d', $id)); $handle->setName($name); + $handle->setURI($service->getURI()); } } diff --git a/src/applications/almanac/query/AlmanacBindingQuery.php b/src/applications/almanac/query/AlmanacBindingQuery.php new file mode 100644 index 0000000000..28bb25f598 --- /dev/null +++ b/src/applications/almanac/query/AlmanacBindingQuery.php @@ -0,0 +1,143 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withServicePHIDs(array $phids) { + $this->servicePHIDs = $phids; + return $this; + } + + public function withDevicePHIDs(array $phids) { + $this->devicePHIDs = $phids; + return $this; + } + + public function withInterfacePHIDs(array $phids) { + $this->interfacePHIDs = $phids; + return $this; + } + + protected function loadPage() { + $table = new AlmanacBinding(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function willFilterPage(array $bindings) { + $service_phids = mpull($bindings, 'getServicePHID'); + $device_phids = mpull($bindings, 'getDevicePHID'); + $interface_phids = mpull($bindings, 'getInterfacePHID'); + + $services = id(new AlmanacServiceQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($service_phids) + ->execute(); + $services = mpull($services, null, 'getPHID'); + + $devices = id(new AlmanacDeviceQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($device_phids) + ->execute(); + $devices = mpull($devices, null, 'getPHID'); + + $interfaces = id(new AlmanacInterfaceQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($interface_phids) + ->execute(); + $interfaces = mpull($interfaces, null, 'getPHID'); + + foreach ($bindings as $key => $binding) { + $service = idx($services, $binding->getServicePHID()); + $device = idx($devices, $binding->getDevicePHID()); + $interface = idx($interfaces, $binding->getInterfacePHID()); + if (!$service || !$device || !$interface) { + $this->didRejectResult($binding); + unset($bindings[$key]); + continue; + } + + $binding->attachService($service); + $binding->attachDevice($device); + $binding->attachInterface($interface); + } + + return $bindings; + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->servicePHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'servicePHID IN (%Ls)', + $this->servicePHIDs); + } + + if ($this->devicePHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'devicePHID IN (%Ls)', + $this->devicePHIDs); + } + + if ($this->interfacePHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'interfacePHID IN (%Ls)', + $this->interfacePHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + +} diff --git a/src/applications/almanac/query/AlmanacBindingTransactionQuery.php b/src/applications/almanac/query/AlmanacBindingTransactionQuery.php new file mode 100644 index 0000000000..2b002e4f4a --- /dev/null +++ b/src/applications/almanac/query/AlmanacBindingTransactionQuery.php @@ -0,0 +1,10 @@ +ids = $ids; @@ -22,6 +23,11 @@ final class AlmanacDeviceQuery return $this; } + public function withDatasourceQuery($query) { + $this->datasourceQuery = $query; + return $this; + } + protected function loadPage() { $table = new AlmanacDevice(); $conn_r = $table->establishConnection('r'); @@ -65,6 +71,13 @@ final class AlmanacDeviceQuery $hashes); } + if ($this->datasourceQuery !== null) { + $where[] = qsprintf( + $conn_r, + 'name LIKE %>', + $this->datasourceQuery); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php new file mode 100644 index 0000000000..74e7e57de7 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -0,0 +1,112 @@ +setServicePHID($service->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'mailKey' => 'bytes20', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_service' => array( + 'columns' => array('servicePHID', 'interfacePHID'), + 'unique' => true, + ), + 'key_device' => array( + 'columns' => array('devicePHID'), + ), + 'key_interface' => array( + 'columns' => array('interfacePHID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID(AlmanacBindingPHIDType::TYPECONST); + } + + public function save() { + if (!$this->mailKey) { + $this->mailKey = Filesystem::readRandomCharacters(20); + } + return parent::save(); + } + + public function getURI() { + return '/almanac/binding/'.$this->getID().'/'; + } + + public function getService() { + return $this->assertAttached($this->service); + } + + public function attachService(AlmanacService $service) { + $this->service = $service; + return $this; + } + + public function getDevice() { + return $this->assertAttached($this->device); + } + + public function attachDevice(AlmanacDevice $device) { + $this->device = $device; + return $this; + } + + public function getInterface() { + return $this->assertAttached($this->interface); + } + + public function attachInterface(AlmanacInterface $interface) { + $this->interface = $interface; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getService()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getService()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return array( + pht('A binding inherits the policies of its service.'), + pht( + 'To view a binding, you must also be able to view its device and '. + 'interface.'), + ); + } + +} diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php new file mode 100644 index 0000000000..4ea4909bb6 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -0,0 +1,65 @@ +getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_INTERFACE: + if ($old) { + $phids[] = $old; + } + if ($new) { + $phids[] = $new; + } + break; + } + + return $phids; + } + + public function getTitle() { + $author_phid = $this->getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_INTERFACE: + if ($old === null) { + return pht( + '%s created this binding.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s changed this binding from %s to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } + break; + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 2066c655b7..aa95cb4236 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -52,8 +52,8 @@ final class AlmanacInterface return $this->assertAttached($this->network); } - public function attachNetwork(AlmanacNetwork $device) { - $this->device = $device; + public function attachNetwork(AlmanacNetwork $network) { + $this->network = $network; return $this; } @@ -68,6 +68,10 @@ final class AlmanacInterface return $this->toAddress()->toHash(); } + public function renderDisplayAddress() { + return $this->getAddress().':'.$this->getPort(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php new file mode 100644 index 0000000000..4a3053d357 --- /dev/null +++ b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php @@ -0,0 +1,51 @@ +getViewer(); + $raw_query = $this->getRawQuery(); + + $devices = id(new AlmanacDeviceQuery()) + ->setViewer($viewer) + ->withDatasourceQuery($raw_query) + ->execute(); + + if ($devices) { + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withDevicePHIDs(mpull($devices, 'getPHID')) + ->execute(); + } else { + $interfaces = array(); + } + + if ($interfaces) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($interfaces, 'getPHID')) + ->execute(); + } else { + $handles = array(); + } + + $results = array(); + foreach ($handles as $handle) { + $results[] = id(new PhabricatorTypeaheadResult()) + ->setName($handle->getName()) + ->setPHID($handle->getPHID()); + } + + return $results; + } + +} diff --git a/src/applications/almanac/view/AlmanacBindingTableView.php b/src/applications/almanac/view/AlmanacBindingTableView.php new file mode 100644 index 0000000000..d6c27f62c9 --- /dev/null +++ b/src/applications/almanac/view/AlmanacBindingTableView.php @@ -0,0 +1,86 @@ +noDataString = $no_data_string; + return $this; + } + + public function getNoDataString() { + return $this->noDataString; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function getHandles() { + return $this->handles; + } + + public function setBindings(array $bindings) { + $this->bindings = $bindings; + return $this; + } + + public function getBindings() { + return $this->bindings; + } + + public function render() { + $bindings = $this->getBindings(); + $handles = $this->getHandles(); + $viewer = $this->getUser(); + + $rows = array(); + foreach ($bindings as $binding) { + $addr = $binding->getInterface()->getAddress(); + $port = $binding->getInterface()->getPort(); + + $rows[] = array( + $binding->getID(), + $handles[$binding->getServicePHID()]->renderLink(), + $handles[$binding->getDevicePHID()]->renderLink(), + $handles[$binding->getInterface()->getNetworkPHID()]->renderLink(), + $binding->getInterface()->renderDisplayAddress(), + phutil_tag( + 'a', + array( + 'class' => 'small grey button', + 'href' => '/almanac/binding/'.$binding->getID().'/', + ), + pht('Details')), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString($this->getNoDataString()) + ->setHeaders( + array( + pht('ID'), + pht('Service'), + pht('Device'), + pht('Network'), + pht('Interface'), + null, + )) + ->setColumnClasses( + array( + '', + '', + '', + '', + 'wide', + 'action', + )); + + return $table; + } + +}