mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-25 08:12:40 +01:00
Implement storage of a host ID and a public key for authorizing Conduit between servers
Summary: Ref T4209. This creates storage for public keys against authorized hosts, such that servers can be authorized to make Conduit calls as the omnipotent user. Servers are registered into this system by running the following command once: ``` bin/almanac register ``` NOTE: This doesn't implement authorization between servers, just the storage of public keys. Placing this against Almanac seemed like the most sensible place, since I'm imagining in future that the `register` command will accept more information (like the hostname of the server so it can be found in the service directory). Test Plan: Ran `bin/almanac register` and saw the host (and public key information) appear in the database. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T4209 Differential Revision: https://secure.phabricator.com/D10400
This commit is contained in:
parent
0ddb187508
commit
8fbebce501
15 changed files with 373 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,6 +13,8 @@
|
||||||
/conf/local/local.json
|
/conf/local/local.json
|
||||||
/conf/local/ENVIRONMENT
|
/conf/local/ENVIRONMENT
|
||||||
/conf/local/VERSION
|
/conf/local/VERSION
|
||||||
|
/conf/local/HOSTKEY
|
||||||
|
/conf/local/HOSTID
|
||||||
|
|
||||||
# Impact Font
|
# Impact Font
|
||||||
/resources/font/impact.ttf
|
/resources/font/impact.ttf
|
||||||
|
|
1
bin/almanac
Symbolic link
1
bin/almanac
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../scripts/almanac/manage_almanac.php
|
18
resources/sql/autopatches/20140902.almanacdevice.1.sql
Normal file
18
resources/sql/autopatches/20140902.almanacdevice.1.sql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_almanac.almanac_device (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
phid VARBINARY(64) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
dateCreated INT UNSIGNED NOT NULL,
|
||||||
|
dateModified INT UNSIGNED NOT NULL,
|
||||||
|
UNIQUE KEY `key_phid` (phid)
|
||||||
|
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
||||||
|
|
||||||
|
CREATE TABLE {$NAMESPACE}_almanac.almanac_deviceproperty (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
devicePHID VARBINARY(64) NOT NULL,
|
||||||
|
`key` VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
value LONGTEXT NOT NULL,
|
||||||
|
dateCreated INT UNSIGNED NOT NULL,
|
||||||
|
dateModified INT UNSIGNED NOT NULL,
|
||||||
|
KEY `key_device` (devicePHID, `key`)
|
||||||
|
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
21
scripts/almanac/manage_almanac.php
Executable file
21
scripts/almanac/manage_almanac.php
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$root = dirname(dirname(dirname(__FILE__)));
|
||||||
|
require_once $root.'/scripts/__init_script__.php';
|
||||||
|
|
||||||
|
$args = new PhutilArgumentParser($argv);
|
||||||
|
$args->setTagline('manage host directory');
|
||||||
|
$args->setSynopsis(<<<EOSYNOPSIS
|
||||||
|
**almanac** __commmand__ [__options__]
|
||||||
|
Manage Almanac stuff. NEW AND EXPERIMENTAL.
|
||||||
|
|
||||||
|
EOSYNOPSIS
|
||||||
|
);
|
||||||
|
$args->parseStandardArguments();
|
||||||
|
|
||||||
|
$workflows = id(new PhutilSymbolLoader())
|
||||||
|
->setAncestorClass('AlmanacManagementWorkflow')
|
||||||
|
->loadObjects();
|
||||||
|
$workflows[] = new PhutilHelpArgumentWorkflow();
|
||||||
|
$args->parseWorkflows($workflows);
|
|
@ -9,6 +9,14 @@
|
||||||
phutil_register_library_map(array(
|
phutil_register_library_map(array(
|
||||||
'__library_version__' => 2,
|
'__library_version__' => 2,
|
||||||
'class' => array(
|
'class' => array(
|
||||||
|
'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php',
|
||||||
|
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
|
||||||
|
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
|
||||||
|
'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
|
||||||
|
'AlmanacDeviceProperty' => 'applications/almanac/storage/AlmanacDeviceProperty.php',
|
||||||
|
'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
|
||||||
|
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
|
||||||
|
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
|
||||||
'Aphront304Response' => 'aphront/response/Aphront304Response.php',
|
'Aphront304Response' => 'aphront/response/Aphront304Response.php',
|
||||||
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
|
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
|
||||||
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
|
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
|
||||||
|
@ -1126,6 +1134,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
|
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
|
||||||
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
|
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
|
||||||
'PhabricatorAllCapsTranslation' => 'infrastructure/internationalization/translation/PhabricatorAllCapsTranslation.php',
|
'PhabricatorAllCapsTranslation' => 'infrastructure/internationalization/translation/PhabricatorAllCapsTranslation.php',
|
||||||
|
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
|
||||||
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
|
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
|
||||||
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
|
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
|
||||||
'PhabricatorAphlictManagementBuildWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementBuildWorkflow.php',
|
'PhabricatorAphlictManagementBuildWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementBuildWorkflow.php',
|
||||||
|
@ -2847,6 +2856,17 @@ phutil_register_library_map(array(
|
||||||
'require_celerity_resource' => 'infrastructure/celerity/api.php',
|
'require_celerity_resource' => 'infrastructure/celerity/api.php',
|
||||||
),
|
),
|
||||||
'xmap' => array(
|
'xmap' => array(
|
||||||
|
'AlmanacConduitUtil' => 'Phobject',
|
||||||
|
'AlmanacDAO' => 'PhabricatorLiskDAO',
|
||||||
|
'AlmanacDevice' => array(
|
||||||
|
'AlmanacDAO',
|
||||||
|
'PhabricatorPolicyInterface',
|
||||||
|
),
|
||||||
|
'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
|
||||||
|
'AlmanacDeviceProperty' => 'AlmanacDAO',
|
||||||
|
'AlmanacDeviceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
|
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||||
'Aphront304Response' => 'AphrontResponse',
|
'Aphront304Response' => 'AphrontResponse',
|
||||||
'Aphront400Response' => 'AphrontResponse',
|
'Aphront400Response' => 'AphrontResponse',
|
||||||
'Aphront403Response' => 'AphrontHTMLResponse',
|
'Aphront403Response' => 'AphrontHTMLResponse',
|
||||||
|
@ -4031,6 +4051,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorActionListView' => 'AphrontView',
|
'PhabricatorActionListView' => 'AphrontView',
|
||||||
'PhabricatorActionView' => 'AphrontView',
|
'PhabricatorActionView' => 'AphrontView',
|
||||||
'PhabricatorAllCapsTranslation' => 'PhabricatorTranslation',
|
'PhabricatorAllCapsTranslation' => 'PhabricatorTranslation',
|
||||||
|
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
|
||||||
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
||||||
'PhabricatorAnchorView' => 'AphrontView',
|
'PhabricatorAnchorView' => 'AphrontView',
|
||||||
'PhabricatorAphlictManagementBuildWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
'PhabricatorAphlictManagementBuildWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAlmanacApplication extends PhabricatorApplication {
|
||||||
|
|
||||||
|
public function getBaseURI() {
|
||||||
|
return '/almanac/';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return pht('Almanac');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShortDescription() {
|
||||||
|
return pht('Service Directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIconName() {
|
||||||
|
return 'almanac';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitleGlyph() {
|
||||||
|
return "\xE2\x98\x82";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationGroup() {
|
||||||
|
return self::GROUP_UTILITIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPrototype() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLaunchable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutes() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacManagementRegisterWorkflow
|
||||||
|
extends AlmanacManagementWorkflow {
|
||||||
|
|
||||||
|
public function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('register')
|
||||||
|
->setSynopsis(pht('Register this host for authorized Conduit access.'))
|
||||||
|
->setArguments(array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$console = PhutilConsole::getConsole();
|
||||||
|
|
||||||
|
if (Filesystem::pathExists(AlmanacConduitUtil::getHostPrivateKeyPath())) {
|
||||||
|
throw new Exception(
|
||||||
|
'This host already has a private key for Conduit access.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pair = PhabricatorSSHKeyGenerator::generateKeypair();
|
||||||
|
list($public_key, $private_key) = $pair;
|
||||||
|
|
||||||
|
$host = id(new AlmanacDevice())
|
||||||
|
->setName(php_uname('n'))
|
||||||
|
->save();
|
||||||
|
|
||||||
|
id(new AlmanacDeviceProperty())
|
||||||
|
->setDevicePHID($host->getPHID())
|
||||||
|
->setKey('conduitPublicOpenSSHKey')
|
||||||
|
->setValue($public_key)
|
||||||
|
->save();
|
||||||
|
|
||||||
|
id(new AlmanacDeviceProperty())
|
||||||
|
->setDevicePHID($host->getPHID())
|
||||||
|
->setKey('conduitPublicOpenSSLKey')
|
||||||
|
->setValue($this->convertToOpenSSLPublicKey($public_key))
|
||||||
|
->save();
|
||||||
|
|
||||||
|
Filesystem::writeFile(
|
||||||
|
AlmanacConduitUtil::getHostPrivateKeyPath(),
|
||||||
|
$private_key);
|
||||||
|
|
||||||
|
Filesystem::writeFile(
|
||||||
|
AlmanacConduitUtil::getHostIDPath(),
|
||||||
|
$host->getID());
|
||||||
|
|
||||||
|
$console->writeOut("Registered as device %d.\n", $host->getID());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertToOpenSSLPublicKey($openssh_public_key) {
|
||||||
|
$ssh_public_key_file = new TempFile();
|
||||||
|
Filesystem::writeFile($ssh_public_key_file, $openssh_public_key);
|
||||||
|
|
||||||
|
list($public_key, $stderr) = id(new ExecFuture(
|
||||||
|
'ssh-keygen -e -f %s -m pkcs8',
|
||||||
|
$ssh_public_key_file))->resolvex();
|
||||||
|
|
||||||
|
unset($ssh_public_key_file);
|
||||||
|
|
||||||
|
return $public_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class AlmanacManagementWorkflow
|
||||||
|
extends PhabricatorManagementWorkflow {}
|
39
src/applications/almanac/phid/AlmanacDevicePHIDType.php
Normal file
39
src/applications/almanac/phid/AlmanacDevicePHIDType.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacDevicePHIDType extends PhabricatorPHIDType {
|
||||||
|
|
||||||
|
const TYPECONST = 'ADEV';
|
||||||
|
|
||||||
|
public function getTypeName() {
|
||||||
|
return pht('Almanac Device');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newObject() {
|
||||||
|
return new AlmanacDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQueryForObjects(
|
||||||
|
PhabricatorObjectQuery $query,
|
||||||
|
array $phids) {
|
||||||
|
|
||||||
|
return id(new AlmanacDeviceQuery())
|
||||||
|
->withPHIDs($phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadHandles(
|
||||||
|
PhabricatorHandleQuery $query,
|
||||||
|
array $handles,
|
||||||
|
array $objects) {
|
||||||
|
|
||||||
|
foreach ($handles as $phid => $handle) {
|
||||||
|
$device = $objects[$phid];
|
||||||
|
|
||||||
|
$id = $device->getID();
|
||||||
|
$name = $device->getName();
|
||||||
|
|
||||||
|
$handle->setObjectName(pht('Device %d', $id));
|
||||||
|
$handle->setName($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
src/applications/almanac/query/AlmanacDeviceQuery.php
Normal file
60
src/applications/almanac/query/AlmanacDeviceQuery.php
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacDeviceQuery
|
||||||
|
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||||
|
|
||||||
|
private $ids;
|
||||||
|
private $phids;
|
||||||
|
|
||||||
|
public function withIDs(array $ids) {
|
||||||
|
$this->ids = $ids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPHIDs(array $phids) {
|
||||||
|
$this->phids = $phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadPage() {
|
||||||
|
$table = new AlmanacDevice();
|
||||||
|
$conn_r = $table->establishConnection('r');
|
||||||
|
|
||||||
|
$data = queryfx_all(
|
||||||
|
$conn_r,
|
||||||
|
'SELECT * FROM %T %Q %Q %Q',
|
||||||
|
$table->getTableName(),
|
||||||
|
$this->buildWhereClause($conn_r),
|
||||||
|
$this->buildOrderClause($conn_r),
|
||||||
|
$this->buildLimitClause($conn_r));
|
||||||
|
|
||||||
|
return $table->loadAllFromArray($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildWhereClause($conn_r) {
|
||||||
|
$where = array();
|
||||||
|
|
||||||
|
if ($this->ids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'id IN (%Ld)',
|
||||||
|
$this->ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->phids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'phid IN (%Ls)',
|
||||||
|
$this->phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
$where[] = $this->buildPagingClause($conn_r);
|
||||||
|
|
||||||
|
return $this->formatWhereClause($where);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryApplicationClass() {
|
||||||
|
return 'PhabricatorAlmanacApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/applications/almanac/storage/AlmanacDAO.php
Normal file
9
src/applications/almanac/storage/AlmanacDAO.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class AlmanacDAO extends PhabricatorLiskDAO {
|
||||||
|
|
||||||
|
public function getApplicationName() {
|
||||||
|
return 'almanac';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
50
src/applications/almanac/storage/AlmanacDevice.php
Normal file
50
src/applications/almanac/storage/AlmanacDevice.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacDevice
|
||||||
|
extends AlmanacDAO
|
||||||
|
implements PhabricatorPolicyInterface {
|
||||||
|
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
public function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
|
'name' => 'text255',
|
||||||
|
),
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatePHID() {
|
||||||
|
return PhabricatorPHID::generateNewPHID(AlmanacDevicePHIDType::TYPECONST);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function getCapabilities() {
|
||||||
|
return array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy($capability) {
|
||||||
|
switch ($capability) {
|
||||||
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
|
// Until we get a clearer idea on what's going to be stored in this
|
||||||
|
// table, don't allow anyone (other than the omnipotent user) to find
|
||||||
|
// these objects.
|
||||||
|
return PhabricatorPolicies::POLICY_NOONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function describeAutomaticCapability($capability) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/applications/almanac/storage/AlmanacDeviceProperty.php
Normal file
25
src/applications/almanac/storage/AlmanacDeviceProperty.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacDeviceProperty extends AlmanacDAO {
|
||||||
|
|
||||||
|
protected $devicePHID;
|
||||||
|
protected $key;
|
||||||
|
protected $value;
|
||||||
|
|
||||||
|
public function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_SERIALIZATION => array(
|
||||||
|
'value' => self::SERIALIZATION_JSON,
|
||||||
|
),
|
||||||
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
|
'key' => 'text128',
|
||||||
|
),
|
||||||
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
|
'key_device' => array(
|
||||||
|
'columns' => array('devicePHID', 'key'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
src/applications/almanac/util/AlmanacConduitUtil.php
Normal file
17
src/applications/almanac/util/AlmanacConduitUtil.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacConduitUtil extends Phobject {
|
||||||
|
|
||||||
|
public static function getHostPrivateKeyPath() {
|
||||||
|
$root = dirname(phutil_get_library_root('phabricator'));
|
||||||
|
$path = $root.'/conf/local/HOSTKEY';
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getHostIDPath() {
|
||||||
|
$root = dirname(phutil_get_library_root('phabricator'));
|
||||||
|
$path = $root.'/conf/local/HOSTID';
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -120,6 +120,7 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
|
||||||
'db.dashboard' => array(),
|
'db.dashboard' => array(),
|
||||||
'db.system' => array(),
|
'db.system' => array(),
|
||||||
'db.fund' => array(),
|
'db.fund' => array(),
|
||||||
|
'db.almanac' => array(),
|
||||||
'0000.legacy.sql' => array(
|
'0000.legacy.sql' => array(
|
||||||
'legacy' => 0,
|
'legacy' => 0,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue