1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 19:40:55 +01:00

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
This commit is contained in:
epriestley 2016-02-21 12:15:57 -08:00
parent 50debecf52
commit db50d0fb11
29 changed files with 1195 additions and 40 deletions

View file

@ -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};

View file

@ -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};

View file

@ -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};

View file

@ -28,6 +28,7 @@ phutil_register_library_map(array(
'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php',
'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php',
'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php',
'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php',
@ -61,6 +62,19 @@ phutil_register_library_map(array(
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.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', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php',
'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php',
@ -4000,6 +4014,7 @@ phutil_register_library_map(array(
), ),
'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCustomField' => 'PhabricatorCustomField', 'AlmanacCustomField' => 'PhabricatorCustomField',
@ -4047,6 +4062,28 @@ phutil_register_library_map(array(
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
'AlmanacNames' => 'Phobject', 'AlmanacNames' => 'Phobject',
'AlmanacNamesTestCase' => 'PhabricatorTestCase', '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( 'AlmanacNetwork' => array(
'AlmanacDAO', 'AlmanacDAO',
'PhabricatorApplicationTransactionInterface', 'PhabricatorApplicationTransactionInterface',

View file

@ -29,7 +29,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
public function getHelpDocumentationArticles(PhabricatorUser $viewer) { public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array( return array(
array( array(
'name' => pht('Alamanac User Guide'), 'name' => pht('Almanac User Guide'),
'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'),
), ),
); );
@ -44,12 +44,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
'/almanac/' => array( '/almanac/' => array(
'' => 'AlmanacConsoleController', '' => 'AlmanacConsoleController',
'service/' => array( 'service/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'AlmanacServiceListController', $this->getQueryRoutePattern() => 'AlmanacServiceListController',
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacServiceEditController', 'edit/(?:(?P<id>\d+)/)?' => 'AlmanacServiceEditController',
'view/(?P<name>[^/]+)/' => 'AlmanacServiceViewController', 'view/(?P<name>[^/]+)/' => 'AlmanacServiceViewController',
), ),
'device/' => array( 'device/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'AlmanacDeviceListController', $this->getQueryRoutePattern() => 'AlmanacDeviceListController',
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacDeviceEditController', 'edit/(?:(?P<id>\d+)/)?' => 'AlmanacDeviceEditController',
'view/(?P<name>[^/]+)/' => 'AlmanacDeviceViewController', 'view/(?P<name>[^/]+)/' => 'AlmanacDeviceViewController',
), ),
@ -61,7 +61,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
'(?P<id>\d+)/' => 'AlmanacBindingViewController', '(?P<id>\d+)/' => 'AlmanacBindingViewController',
), ),
'network/' => array( 'network/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'AlmanacNetworkListController', $this->getQueryRoutePattern() => 'AlmanacNetworkListController',
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacNetworkEditController', 'edit/(?:(?P<id>\d+)/)?' => 'AlmanacNetworkEditController',
'(?P<id>\d+)/' => 'AlmanacNetworkViewController', '(?P<id>\d+)/' => 'AlmanacNetworkViewController',
), ),
@ -69,6 +69,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
'edit/' => 'AlmanacPropertyEditController', 'edit/' => 'AlmanacPropertyEditController',
'delete/' => 'AlmanacPropertyDeleteController', 'delete/' => 'AlmanacPropertyDeleteController',
), ),
'namespace/' => array(
$this->getQueryRoutePattern() => 'AlmanacNamespaceListController',
$this->getEditRoutePattern('edit/')
=> 'AlmanacNamespaceEditController',
'(?P<id>\d+)/' => 'AlmanacNamespaceViewController',
),
), ),
); );
} }
@ -84,6 +90,9 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
AlmanacCreateNetworksCapability::CAPABILITY => array( AlmanacCreateNetworksCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN, 'default' => PhabricatorPolicies::POLICY_ADMIN,
), ),
AlmanacCreateNamespacesCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
AlmanacCreateClusterServicesCapability::CAPABILITY => array( AlmanacCreateClusterServicesCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN, 'default' => PhabricatorPolicies::POLICY_ADMIN,
), ),

View file

@ -0,0 +1,16 @@
<?php
final class AlmanacCreateNamespacesCapability
extends PhabricatorPolicyCapability {
const CAPABILITY = 'almanac.namespaces';
public function getCapabilityName() {
return pht('Can Create Namespaces');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to create Almanac namespaces.');
}
}

View file

@ -12,26 +12,52 @@ final class AlmanacConsoleController extends AlmanacController {
$menu = id(new PHUIObjectItemListView()) $menu = id(new PHUIObjectItemListView())
->setUser($viewer); ->setUser($viewer);
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Services'))
->setHref($this->getApplicationURI('service/'))
->setIcon('fa-plug')
->addAttribute(pht('Manage Almanac services.')));
$menu->addItem( $menu->addItem(
id(new PHUIObjectItemView()) id(new PHUIObjectItemView())
->setHeader(pht('Devices')) ->setHeader(pht('Devices'))
->setHref($this->getApplicationURI('device/')) ->setHref($this->getApplicationURI('device/'))
->setIcon('fa-server') ->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( $menu->addItem(
id(new PHUIObjectItemView()) id(new PHUIObjectItemView())
->setHeader(pht('Networks')) ->setHeader(pht('Networks'))
->setHref($this->getApplicationURI('network/')) ->setHref($this->getApplicationURI('network/'))
->setIcon('fa-globe') ->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 = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console')); $crumbs->addTextCrumb(pht('Console'));

View file

@ -0,0 +1,14 @@
<?php
abstract class AlmanacNamespaceController extends AlmanacController {
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$list_uri = $this->getApplicationURI('namespace/');
$crumbs->addTextCrumb(pht('Namespaces'), $list_uri);
return $crumbs;
}
}

View file

@ -0,0 +1,11 @@
<?php
final class AlmanacNamespaceEditController extends AlmanacController {
public function handleRequest(AphrontRequest $request) {
return id(new AlmanacNamespaceEditEngine())
->setController($this)
->buildResponse();
}
}

View file

@ -0,0 +1,26 @@
<?php
final class AlmanacNamespaceListController
extends AlmanacNamespaceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
return id(new AlmanacNamespaceSearchEngine())
->setController($this)
->buildResponse();
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
id(new AlmanacNamespaceEditEngine())
->setViewer($this->getViewer())
->addActionToCrumbs($crumbs);
return $crumbs;
}
}

View file

@ -0,0 +1,87 @@
<?php
final class AlmanacNamespaceViewController
extends AlmanacNamespaceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->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;
}
}

View file

@ -55,7 +55,7 @@ final class AlmanacPropertyEditController
} else { } else {
$caught = null; $caught = null;
try { try {
AlmanacNames::validateServiceOrDeviceName($name); AlmanacNames::validateName($name);
} catch (Exception $ex) { } catch (Exception $ex) {
$caught = $ex; $caught = $ex;
} }
@ -92,7 +92,7 @@ final class AlmanacPropertyEditController
// Make sure property key is appropriate. // Make sure property key is appropriate.
// TODO: It would be cleaner to put this safety check in the Editor. // 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 // If we're adding a new property, put a placeholder on the object so
// that we can build a CustomField for it. // that we can build a CustomField for it.

View file

@ -136,7 +136,7 @@ final class AlmanacDeviceEditor
$name = $xaction->getNewValue(); $name = $xaction->getNewValue();
try { try {
AlmanacNames::validateServiceOrDeviceName($name); AlmanacNames::validateName($name);
} catch (Exception $ex) { } catch (Exception $ex) {
$message = $ex->getMessage(); $message = $ex->getMessage();
} }

View file

@ -0,0 +1,86 @@
<?php
final class AlmanacNamespaceEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'almanac.namespace';
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Almanac Namespaces');
}
public function getSummaryHeader() {
return pht('Edit Almanac Namespace Configurations');
}
public function getSummaryText() {
return pht('This engine is used to edit Almanac namespaces.');
}
public function getEngineApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
protected function newEditableObject() {
return AlmanacNamespace::initializeNewNamespace();
}
protected function newObjectQuery() {
return new AlmanacNamespaceQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Namespace');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Namespace');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Namespace: %s', $object->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()),
);
}
}

View file

@ -0,0 +1,162 @@
<?php
final class AlmanacNamespaceEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
public function getEditorObjectsDescription() {
return pht('Almanac Namespace');
}
protected function supportsSearch() {
return true;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = AlmanacNamespaceTransaction::TYPE_NAME;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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);
}
}

View file

@ -128,7 +128,7 @@ final class AlmanacServiceEditor
$name = $xaction->getNewValue(); $name = $xaction->getNewValue();
try { try {
AlmanacNames::validateServiceOrDeviceName($name); AlmanacNames::validateName($name);
} catch (Exception $ex) { } catch (Exception $ex) {
$message = $ex->getMessage(); $message = $ex->getMessage();
} }

View file

@ -0,0 +1,44 @@
<?php
final class AlmanacNamespacePHIDType extends PhabricatorPHIDType {
const TYPECONST = 'ANAM';
public function getTypeName() {
return pht('Almanac Namespace');
}
public function newObject() {
return new AlmanacNamespace();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new AlmanacNamespaceQuery())
->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());
}
}
}

View file

@ -0,0 +1,103 @@
<?php
final class AlmanacNamespaceQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $names;
public function withIDs(array $ids) {
$this->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';
}
}

View file

@ -0,0 +1,90 @@
<?php
final class AlmanacNamespaceSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Almanac Namespaces');
}
public function getApplicationClassName() {
return 'PhabricatorAlmanacApplication';
}
public function newQuery() {
return new AlmanacNamespaceQuery();
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchTextField())
->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;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class AlmanacNamespaceTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new AlmanacNamespaceTransaction();
}
}

View file

@ -20,7 +20,7 @@ final class AlmanacNetworkSearchEngine
id(new PhabricatorSearchTextField()) id(new PhabricatorSearchTextField())
->setLabel(pht('Name Contains')) ->setLabel(pht('Name Contains'))
->setKey('match') ->setKey('match')
->setDescription(pht('Search for devices by name substring.')), ->setDescription(pht('Search for networks by name substring.')),
); );
} }

View file

@ -56,7 +56,7 @@ final class AlmanacDevice
} }
public function save() { public function save() {
AlmanacNames::validateServiceOrDeviceName($this->getName()); AlmanacNames::validateName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName());

View file

@ -0,0 +1,197 @@
<?php
final class AlmanacNamespace
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
AlmanacPropertyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $customFields = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewNamespace() {
return id(new self())
->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()),
);
}
}

View file

@ -0,0 +1,18 @@
<?php
final class AlmanacNamespaceNameNgrams
extends PhabricatorSearchNgrams {
public function getNgramKey() {
return 'namespacename';
}
public function getColumnName() {
return 'name';
}
public function getApplicationName() {
return 'almanac';
}
}

View file

@ -0,0 +1,43 @@
<?php
final class AlmanacNamespaceTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'almanac:namespace:name';
public function getApplicationName() {
return 'almanac';
}
public function getApplicationTransactionType() {
return AlmanacNamespacePHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getTitle() {
$author_phid = $this->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();
}
}

View file

@ -62,7 +62,7 @@ final class AlmanacService
} }
public function save() { public function save() {
AlmanacNames::validateServiceOrDeviceName($this->getName()); AlmanacNames::validateName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName());

View file

@ -2,54 +2,61 @@
final class AlmanacNames extends Phobject { final class AlmanacNames extends Phobject {
public static function validateServiceOrDeviceName($name) { public static function validateName($name) {
if (strlen($name) < 3) { if (strlen($name) < 3) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names must be at least 3 '. 'Almanac service, device, property and namespace names must be '.
'characters long.')); '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)) { if (!preg_match('/^[a-z0-9.-]+\z/', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names may only contain lowercase '. 'Almanac service, device, property and namespace names may only '.
'letters, numbers, hyphens, and periods.')); 'contain lowercase letters, numbers, hyphens, and periods.'));
} }
if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names may not have any segments '. 'Almanac service, device, property and namespace names may not '.
'containing only digits.')); 'have any segments containing only digits.'));
} }
if (preg_match('/\.\./', $name)) { if (preg_match('/\.\./', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names may not contain multiple '. 'Almanac service, device, property and namespace names may not '.
'consecutive periods.')); 'contain multiple consecutive periods.'));
} }
if (preg_match('/\\.-|-\\./', $name)) { if (preg_match('/\\.-|-\\./', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Amanac service and device names may not contain hyphens adjacent '. 'Almanac service, device, property and namespace names may not '.
'to periods.')); 'contain hyphens adjacent to periods.'));
} }
if (preg_match('/--/', $name)) { if (preg_match('/--/', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names may not contain multiple '. 'Almanac service, device, property and namespace names may not '.
'consecutive hyphens.')); 'contain multiple consecutive hyphens.'));
} }
if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) {
throw new Exception( throw new Exception(
pht( pht(
'Almanac service and device names must begin and end with a letter '. 'Almanac service, device, property and namespace names must begin '.
'or number.')); 'and end with a letter or number.'));
} }
} }

View file

@ -33,12 +33,16 @@ final class AlmanacNamesTestCase extends PhabricatorTestCase {
'db.phacility.instance' => true, 'db.phacility.instance' => true,
'web002.useast.example.com' => true, 'web002.useast.example.com' => true,
'master.example-corp.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) { foreach ($map as $input => $expect) {
$caught = null; $caught = null;
try { try {
AlmanacNames::validateServiceOrDeviceName($input); AlmanacNames::validateName($input);
} catch (Exception $ex) { } catch (Exception $ex) {
$caught = $ex; $caught = $ex;
} }

View file

@ -1,13 +1,138 @@
@title Almanac User Guide @title Almanac User Guide
@group userguide @group userguide
Using Almanac to manage services. Using Almanac to manage devices and services.
= Overview = Overview
========
IMPORTANT: Almanac is a prototype application. See IMPORTANT: Almanac is a prototype application. See
@{article:User Guide: Prototype Applications}. @{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 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 For more details on this scenario, see
@{article:User Guide: Phabricator Clusters}. @{article:User Guide: Phabricator Clusters}.
Beyond hardening cluster definitions, you might also want to lock a service to Beyond hardening cluster definitions, you might also want to lock a critical
prevent accidental edits. service to prevent accidental edits.
To lock a service, run: To lock a service, run: