From db50d0fb11e2c01a9014e530237f6b58b98628ea Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 21 Feb 2016 12:15:57 -0800 Subject: [PATCH] Rough-in Almanac namespaces Summary: Ref T6741. Ref T10246. Root problem: to provide Drydock in the cluster, we need to expose Almanac, and doing so would let users accidentally or intentionally create a bunch of `repo006.phacility.net` devices/services which could conflict with the real ones we manage. There's currently no way to say "you can't create anything named `*.blah.net`". This adds "namespaces", which let you do that (well, not yet, but they will after the next diff). After the next diff, if you try to create `repo003.phacility.net`, but the namespace `phacility.net` already exists and you don't have permission to edit it, you'll be asked to choose a different name. Also various modernizations and some new docs. Test Plan: - Created cool namespaces like `this.computer`. - Almanac namespaces don't actually enforce policies yet. Reviewers: chad Reviewed By: chad Maniphest Tasks: T6741, T10246 Differential Revision: https://secure.phabricator.com/D15324 --- .../20160221.almanac.7.namespacen.sql | 7 + .../20160221.almanac.8.namespace.sql | 14 ++ .../20160221.almanac.9.namespacex.sql | 19 ++ src/__phutil_library_map__.php | 37 ++++ .../PhabricatorAlmanacApplication.php | 17 +- .../AlmanacCreateNamespacesCapability.php | 16 ++ .../controller/AlmanacConsoleController.php | 44 +++- .../controller/AlmanacNamespaceController.php | 14 ++ .../AlmanacNamespaceEditController.php | 11 + .../AlmanacNamespaceListController.php | 26 +++ .../AlmanacNamespaceViewController.php | 87 ++++++++ .../AlmanacPropertyEditController.php | 4 +- .../almanac/editor/AlmanacDeviceEditor.php | 2 +- .../editor/AlmanacNamespaceEditEngine.php | 86 ++++++++ .../almanac/editor/AlmanacNamespaceEditor.php | 162 ++++++++++++++ .../almanac/editor/AlmanacServiceEditor.php | 2 +- .../almanac/phid/AlmanacNamespacePHIDType.php | 44 ++++ .../almanac/query/AlmanacNamespaceQuery.php | 103 +++++++++ .../query/AlmanacNamespaceSearchEngine.php | 90 ++++++++ .../AlmanacNamespaceTransactionQuery.php | 10 + .../query/AlmanacNetworkSearchEngine.php | 2 +- .../almanac/storage/AlmanacDevice.php | 2 +- .../almanac/storage/AlmanacNamespace.php | 197 ++++++++++++++++++ .../storage/AlmanacNamespaceNameNgrams.php | 18 ++ .../storage/AlmanacNamespaceTransaction.php | 43 ++++ .../almanac/storage/AlmanacService.php | 2 +- .../almanac/util/AlmanacNames.php | 37 ++-- .../util/__tests__/AlmanacNamesTestCase.php | 6 +- src/docs/user/userguide/almanac.diviner | 133 +++++++++++- 29 files changed, 1195 insertions(+), 40 deletions(-) create mode 100644 resources/sql/autopatches/20160221.almanac.7.namespacen.sql create mode 100644 resources/sql/autopatches/20160221.almanac.8.namespace.sql create mode 100644 resources/sql/autopatches/20160221.almanac.9.namespacex.sql create mode 100644 src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceEditController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceListController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceViewController.php create mode 100644 src/applications/almanac/editor/AlmanacNamespaceEditEngine.php create mode 100644 src/applications/almanac/editor/AlmanacNamespaceEditor.php create mode 100644 src/applications/almanac/phid/AlmanacNamespacePHIDType.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceQuery.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceSearchEngine.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php create mode 100644 src/applications/almanac/storage/AlmanacNamespace.php create mode 100644 src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php create mode 100644 src/applications/almanac/storage/AlmanacNamespaceTransaction.php diff --git a/resources/sql/autopatches/20160221.almanac.7.namespacen.sql b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql new file mode 100644 index 0000000000..bdacc3552c --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.8.namespace.sql b/resources/sql/autopatches/20160221.almanac.8.namespace.sql new file mode 100644 index 0000000000..90f53d3daa --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.8.namespace.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespace ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + nameIndex BINARY(12) NOT NULL, + mailKey BINARY(20) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_nameindex` (nameIndex), + KEY `key_name` (name) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.9.namespacex.sql b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql new file mode 100644 index 0000000000..4f695456f9 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacetransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2dde62fdd0..92943d801d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -28,6 +28,7 @@ phutil_register_library_map(array( 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', + 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', @@ -61,6 +62,19 @@ phutil_register_library_map(array( 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', + 'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php', + 'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php', + 'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php', + 'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php', + 'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php', + 'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php', + 'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php', + 'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php', + 'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php', + 'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php', + 'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php', + 'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php', + 'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', @@ -4000,6 +4014,7 @@ phutil_register_library_map(array( ), 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', + 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCustomField' => 'PhabricatorCustomField', @@ -4047,6 +4062,28 @@ phutil_register_library_map(array( 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', + 'AlmanacNamespace' => array( + 'AlmanacDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorCustomFieldInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorProjectInterface', + 'AlmanacPropertyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', + ), + 'AlmanacNamespaceController' => 'AlmanacController', + 'AlmanacNamespaceEditController' => 'AlmanacController', + 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', + 'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', + 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', + 'AlmanacNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacNamespaceViewController' => 'AlmanacNamespaceController', 'AlmanacNetwork' => array( 'AlmanacDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index a444554a0e..ebcefe4a51 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -29,7 +29,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array( array( - 'name' => pht('Alamanac User Guide'), + 'name' => pht('Almanac User Guide'), 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), ), ); @@ -44,12 +44,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { '/almanac/' => array( '' => 'AlmanacConsoleController', 'service/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacServiceListController', + $this->getQueryRoutePattern() => 'AlmanacServiceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacServiceEditController', 'view/(?P[^/]+)/' => 'AlmanacServiceViewController', ), 'device/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacDeviceListController', + $this->getQueryRoutePattern() => 'AlmanacDeviceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacDeviceEditController', 'view/(?P[^/]+)/' => 'AlmanacDeviceViewController', ), @@ -61,7 +61,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { '(?P\d+)/' => 'AlmanacBindingViewController', ), 'network/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacNetworkListController', + $this->getQueryRoutePattern() => 'AlmanacNetworkListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacNetworkEditController', '(?P\d+)/' => 'AlmanacNetworkViewController', ), @@ -69,6 +69,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { 'edit/' => 'AlmanacPropertyEditController', 'delete/' => 'AlmanacPropertyDeleteController', ), + 'namespace/' => array( + $this->getQueryRoutePattern() => 'AlmanacNamespaceListController', + $this->getEditRoutePattern('edit/') + => 'AlmanacNamespaceEditController', + '(?P\d+)/' => 'AlmanacNamespaceViewController', + ), ), ); } @@ -84,6 +90,9 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { AlmanacCreateNetworksCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), + AlmanacCreateNamespacesCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), AlmanacCreateClusterServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), diff --git a/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php new file mode 100644 index 0000000000..bfff9aaeaf --- /dev/null +++ b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php @@ -0,0 +1,16 @@ +setUser($viewer); - $menu->addItem( - id(new PHUIObjectItemView()) - ->setHeader(pht('Services')) - ->setHref($this->getApplicationURI('service/')) - ->setIcon('fa-plug') - ->addAttribute(pht('Manage Almanac services.'))); - $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Devices')) ->setHref($this->getApplicationURI('device/')) ->setIcon('fa-server') - ->addAttribute(pht('Manage Almanac devices.'))); + ->addAttribute( + pht( + 'Create an inventory of physical and virtual hosts and '. + 'devices.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Services')) + ->setHref($this->getApplicationURI('service/')) + ->setIcon('fa-plug') + ->addAttribute( + pht( + 'Create and update services, and map them to interfaces on '. + 'devices.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Networks')) ->setHref($this->getApplicationURI('network/')) ->setIcon('fa-globe') - ->addAttribute(pht('Manage Almanac networks.'))); + ->addAttribute( + pht( + 'Manage public and private networks.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Namespaces')) + ->setHref($this->getApplicationURI('namespace/')) + ->setIcon('fa-asterisk') + ->addAttribute( + pht('Control who can create new named services and devices.'))); + + $docs_uri = PhabricatorEnv::getDoclink( + 'Almanac User Guide'); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Documentation')) + ->setHref($docs_uri) + ->setIcon('fa-book') + ->addAttribute(pht('Browse documentation for Almanac.'))); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); diff --git a/src/applications/almanac/controller/AlmanacNamespaceController.php b/src/applications/almanac/controller/AlmanacNamespaceController.php new file mode 100644 index 0000000000..abdb8a01e6 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceController.php @@ -0,0 +1,14 @@ +getApplicationURI('namespace/'); + $crumbs->addTextCrumb(pht('Namespaces'), $list_uri); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceEditController.php b/src/applications/almanac/controller/AlmanacNamespaceEditController.php new file mode 100644 index 0000000000..1f3cf84e52 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceEditController.php @@ -0,0 +1,11 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceListController.php b/src/applications/almanac/controller/AlmanacNamespaceListController.php new file mode 100644 index 0000000000..72c9593457 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new AlmanacNamespaceEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceViewController.php b/src/applications/almanac/controller/AlmanacNamespaceViewController.php new file mode 100644 index 0000000000..4f140683a2 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceViewController.php @@ -0,0 +1,87 @@ +getViewer(); + + $id = $request->getURIData('id'); + $namespace = id(new AlmanacNamespaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$namespace) { + return new Aphront404Response(); + } + + $title = pht('Namespace %s', $namespace->getName()); + + $property_list = $this->buildPropertyList($namespace); + $action_list = $this->buildActionList($namespace); + $property_list->setActionList($action_list); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($namespace->getName()) + ->setPolicyObject($namespace); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($property_list); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($namespace->getName()); + + $timeline = $this->buildTransactionTimeline( + $namespace, + new AlmanacNamespaceTransactionQuery()); + $timeline->setShouldTerminate(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $box, + $timeline, + )); + } + + private function buildPropertyList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + return $properties; + } + + private function buildActionList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + $id = $namespace->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $namespace, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Namespace')) + ->setHref($this->getApplicationURI("namespace/edit/{$id}/")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $actions; + } + +} diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index 9a881bc046..aa52dc07d6 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -55,7 +55,7 @@ final class AlmanacPropertyEditController } else { $caught = null; try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $caught = $ex; } @@ -92,7 +92,7 @@ final class AlmanacPropertyEditController // Make sure property key is appropriate. // TODO: It would be cleaner to put this safety check in the Editor. - AlmanacNames::validateServiceOrDeviceName($property_key); + AlmanacNames::validateName($property_key); // If we're adding a new property, put a placeholder on the object so // that we can build a CustomField for it. diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 25d49fb295..055322d8b0 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -136,7 +136,7 @@ final class AlmanacDeviceEditor $name = $xaction->getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php new file mode 100644 index 0000000000..0aaba76d3c --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php @@ -0,0 +1,86 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Namespace'); + } + + protected function getObjectCreateShortText() { + return pht('Create Namespace'); + } + + protected function getEditorURI() { + return '/almanac/namespace/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/namespace/'; + } + + protected function getObjectViewURI($object) { + $id = $object->getID(); + return "/almanac/namespace/{$id}/"; + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AlmanacCreateNamespacesCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the namespace.')) + ->setTransactionType(AlmanacNamespaceTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php new file mode 100644 index 0000000000..b11a081630 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -0,0 +1,162 @@ +getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $object->getName(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Namespace name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } else { + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + + $other = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'The namespace name "%s" is already in use by another '. + 'namespace. Each namespace must have a unique name.', + $name), + $xaction); + $errors[] = $error; + continue; + } + } + } + + break; + } + + return $errors; + } + + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + + $errors = array(); + + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Invalid'), + pht( + 'Another namespace with this name already exists. Each namespace '. + 'must have a unique name.'), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + +} diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 178621fee0..4c230f8b85 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -128,7 +128,7 @@ final class AlmanacServiceEditor $name = $xaction->getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } diff --git a/src/applications/almanac/phid/AlmanacNamespacePHIDType.php b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php new file mode 100644 index 0000000000..aa0b8fc4ef --- /dev/null +++ b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php @@ -0,0 +1,44 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $namespace = $objects[$phid]; + + $id = $namespace->getID(); + $name = $namespace->getName(); + + $handle->setObjectName(pht('Namespace %d', $id)); + $handle->setName($name); + $handle->setURI($namespace->getURI()); + } + } + +} diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php new file mode 100644 index 0000000000..48570c0f80 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -0,0 +1,103 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacNamespaceNameNgrams(), + $ngrams); + } + + public function newResultObject() { + return new AlmanacNamespace(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.phid IN (%Ls)', + $this->phids); + } + + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'namespace.name IN (%Ls)', + $this->names); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'namespace'; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $namespace = $this->loadCursorObject($cursor); + return array( + 'id' => $namespace->getID(), + 'name' => $namespace->getName(), + ); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Namespace Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getQueryApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + +} diff --git a/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php new file mode 100644 index 0000000000..14a96d22a0 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php @@ -0,0 +1,90 @@ +setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for namespaces by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + + return $query; + } + + protected function getURI($path) { + return '/almanac/namespace/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Namespaces'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $namespaces, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($namespaces, 'AlmanacNamespace'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($namespaces as $namespace) { + $id = $namespace->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Namespace %d', $id)) + ->setHeader($namespace->getName()) + ->setHref($this->getApplicationURI("namespace/{$id}/")) + ->setObject($namespace); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No Almanac namespaces found.')); + + return $result; + } +} diff --git a/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php new file mode 100644 index 0000000000..64ce639f45 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php @@ -0,0 +1,10 @@ +setLabel(pht('Name Contains')) ->setKey('match') - ->setDescription(pht('Search for devices by name substring.')), + ->setDescription(pht('Search for networks by name substring.')), ); } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 09503a31b6..425fc32818 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -56,7 +56,7 @@ final class AlmanacDevice } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php new file mode 100644 index 0000000000..ff27d6c52b --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -0,0 +1,197 @@ +setViewPolicy(PhabricatorPolicies::POLICY_USER) + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->attachAlmanacProperties(array()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text128', + 'nameIndex' => 'bytes12', + 'mailKey' => 'bytes20', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_nameindex' => array( + 'columns' => array('nameIndex'), + 'unique' => true, + ), + 'key_name' => array( + 'columns' => array('name'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + AlmanacNamespacePHIDType::TYPECONST); + } + + public function save() { + AlmanacNames::validateName($this->getName()); + + $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); + + if (!$this->mailKey) { + $this->mailKey = Filesystem::readRandomCharacters(20); + } + + return parent::save(); + } + + public function getURI() { + return '/almanac/namespace/view/'.$this->getName().'/'; + } + + +/* -( AlmanacPropertyInterface )------------------------------------------- */ + + + public function attachAlmanacProperties(array $properties) { + assert_instances_of($properties, 'AlmanacProperty'); + $this->almanacProperties = mpull($properties, null, 'getFieldName'); + return $this; + } + + public function getAlmanacProperties() { + return $this->assertAttached($this->almanacProperties); + } + + public function hasAlmanacProperty($key) { + $this->assertAttached($this->almanacProperties); + return isset($this->almanacProperties[$key]); + } + + public function getAlmanacProperty($key) { + return $this->assertAttachedKey($this->almanacProperties, $key); + } + + public function getAlmanacPropertyValue($key, $default = null) { + if ($this->hasAlmanacProperty($key)) { + return $this->getAlmanacProperty($key)->getFieldValue(); + } else { + return $default; + } + } + + public function getAlmanacPropertyFieldSpecifications() { + return array(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + + public function getCustomFieldSpecificationForRole($role) { + return array(); + } + + public function getCustomFieldBaseClass() { + return 'AlmanacCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new AlmanacNamespaceEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new AlmanacNamespaceTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacNamespaceNameNgrams()) + ->setValue($this->getName()), + ); + } + +} diff --git a/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php new file mode 100644 index 0000000000..c7907b931e --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php @@ -0,0 +1,18 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this namespace.', + $this->renderHandleLink($author_phid)); + break; + case self::TYPE_NAME: + return pht( + '%s renamed this namespace from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index c16f85c8ee..765ae58278 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -62,7 +62,7 @@ final class AlmanacService } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php index d65779264d..68a387cb9b 100644 --- a/src/applications/almanac/util/AlmanacNames.php +++ b/src/applications/almanac/util/AlmanacNames.php @@ -2,54 +2,61 @@ final class AlmanacNames extends Phobject { - public static function validateServiceOrDeviceName($name) { + public static function validateName($name) { if (strlen($name) < 3) { throw new Exception( pht( - 'Almanac service and device names must be at least 3 '. - 'characters long.')); + 'Almanac service, device, property and namespace names must be '. + 'at least 3 characters long.')); + } + + if (strlen($name) > 100) { + throw new Exception( + pht( + 'Almanac service, device, property and namespace names may not '. + 'be more than 100 characters long.')); } if (!preg_match('/^[a-z0-9.-]+\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names may only contain lowercase '. - 'letters, numbers, hyphens, and periods.')); + 'Almanac service, device, property and namespace names may only '. + 'contain lowercase letters, numbers, hyphens, and periods.')); } if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not have any segments '. - 'containing only digits.')); + 'Almanac service, device, property and namespace names may not '. + 'have any segments containing only digits.')); } if (preg_match('/\.\./', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive periods.')); } if (preg_match('/\\.-|-\\./', $name)) { throw new Exception( pht( - 'Amanac service and device names may not contain hyphens adjacent '. - 'to periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain hyphens adjacent to periods.')); } if (preg_match('/--/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive hyphens.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive hyphens.')); } if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names must begin and end with a letter '. - 'or number.')); + 'Almanac service, device, property and namespace names must begin '. + 'and end with a letter or number.')); } } diff --git a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php index 7ca1a37f9f..78dea769e4 100644 --- a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php +++ b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php @@ -33,12 +33,16 @@ final class AlmanacNamesTestCase extends PhabricatorTestCase { 'db.phacility.instance' => true, 'web002.useast.example.com' => true, 'master.example-corp.com' => true, + + // Maximum length is 100. + str_repeat('a', 100) => true, + str_repeat('a', 101) => false, ); foreach ($map as $input => $expect) { $caught = null; try { - AlmanacNames::validateServiceOrDeviceName($input); + AlmanacNames::validateName($input); } catch (Exception $ex) { $caught = $ex; } diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index c6f5ef2971..2e950a586a 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -1,13 +1,138 @@ @title Almanac User Guide @group userguide -Using Almanac to manage services. +Using Almanac to manage devices and services. -= Overview = +Overview +======== IMPORTANT: Almanac is a prototype application. See @{article:User Guide: Prototype Applications}. +Almanac is a device and service inventory application. It allows you to create +lists of //devices// and //services// that humans and other applications can +use to keep track of what is running where. + +At a very high level, Almanac can be thought of as a bit like a DNS server. +Callers ask it for information about services, and it responds with details +about which devices host those services. However, it can respond to a broader +range of queries and provide more detailed responses than DNS alone can. + +Today, the primary use cases for Almanac involve configuring Phabricator +itself: Almanac is used to configure Phabricator to operate in a cluster setup, +and to expose hardware to Drydock so it can run build and integration tasks. + +Beyond internal uses, Almanac is a general-purpose service and device inventory +application and can be used to configure and manage other types of service and +hardware inventories, but these use cases are currently considered experimental +and you should be exercise caution in pursuing them. + + +Example: Drydock Build Pool +================================ + +Here's a quick example of how you might configure Almanac to solve a real-world +problem. This section describes configuration at a high level to give you an +introduction to Almanac concepts and a better idea of how the pieces fit +together. + +In this scenario, we want to use Drydock to run some sort of build process. To +do this, Drydock needs hardware to run on. We're going to use Almanac to tell +Drydock about the hardware it should use. + +In this scenario, Almanac will work a bit like a DNS server. When we're done, +Drydock will be able to query Almanac for information about a service (like +`build.mycompany.com`) and get back information about which hosts are part of +that service and where it should connect to. + +Before getting started, we need to create a **network**. For simplicity, let's +suppose everything will be connected through the public internet. If you +haven't already, you'd create a "Public Internet" network first. + +Once we have a network, we create the actual physical or virtual hosts by +launching instances in EC2, or racking and powering on some servers, or already +having some hardware on hand we want to use. We set the hosts up normally and +connect them to the internet or network. + +After the hosts exist, we add them to Almanac as **devices**, like +`build001.mycompany.com`, `build002.mycompany.com`, and so on. In Almanac, +devices are usually physical or virtual hosts, although you could also use it +to inventory other types of devices and hardware. + +For each **device**, we add an **interface**. This is just an address and port +on a particular network. Since we're going to connect to these hosts over +SSH, we'll add interfaces on the standard SSH port 22. An example configuration +might look a little bit like this: + +| Device | Network | Address | Port | +|--------|---------|---------|------| +| `build001.mycompany.com` | Public Internet | 58.8.9.10 | 22 +| `build002.mycompany.com` | Public Internet | 58.8.9.11 | 22 +| ... | Public Internet | ... | 22 + +Now, we create the **service**. This is what we'll tell Drydock about, and +it can query for information about this service to find connected devices. +Here, we'll call it `build.mycompany.com`. + +After creating the service, add **bindings** to the interfaces we configured +above. This will tell Drydock where it should actually connect to. + +Once this is complete, we're done in Almanac and can continue configuration in +Drydock, which is outside the scope of this example. Once everything is fully +configured, this is how Almanac will be used by Drydock: + + - Drydock will query information about `build.mycompany.com` from Almanac. + - Drydock will get back a list of bound interfaces, among other data. + - The interfaces provide information about addresses and ports that Drydock + can use to connect to the actual devices. + +You can now add and remove devices to the pool by binding them and unbinding +them from the service. + + +Concepts +======== + +The major concepts in Almanac are **devices*, **interfaces**, **services**, +**bindings**, **networks**, and **namespaces**. + +**Devices**: Almanac devices represent physical or virtual devices. +Usually, they are hosts (like `web001.mycompany.net`), although you could +use devices to keep inventory of any other kind of device or physical asset +(like phones, laptops, or office chairs). + +Each device has a name, and may have properties and interfaces. + +**Interfaces**: Interfaces are listening address/port combinations on devices. +For example, if you have a webserver host device named `web001.mycompany.net`, +you might add an interface on port `80`. + +Interfaces tell users and applications where they should connect to to access +services and devices. + +**Services**: These are named services like `build.mycompany.net` that work +a bit like DNS. Humans or other applications can look up a service to find +configuration information and learn which devices are hosting the service. + +Each service has a name, and may have properties and bindings. + +**Bindings**: Bindings are connections between services and interfaces. They +tell callers which devices host a named service. + +**Networks**: Networks allow Almanac to distingiush between addresses on +different networks, like VPNs vs the public internet. + +If you have hosts in different VPNs or on private networks, you might have +multiple devices which share the same IP address (like `10.0.0.3`). Networks +allow Almanac to distinguish between devices with the same address on different +sections of the network. + +**Namespaces**: Namespaces let you control who is permitted to create devices +and services with particular names. For example, the namespace `mycompany.com` +controls who can create services with names like `a.mycompany.com` and +`b.mycompany.com`. + + Locking and Unlocking Services ============================== @@ -17,8 +142,8 @@ services prevents an attacker from modifying the Phabricator cluster definition. For more details on this scenario, see @{article:User Guide: Phabricator Clusters}. -Beyond hardening cluster definitions, you might also want to lock a service to -prevent accidental edits. +Beyond hardening cluster definitions, you might also want to lock a critical +service to prevent accidental edits. To lock a service, run: