2016-10-13 19:49:04 +00:00
|
|
|
|
{ lib, ... }:
|
2015-07-24 10:23:52 +00:00
|
|
|
|
|
2015-09-27 13:24:41 +00:00
|
|
|
|
with builtins;
|
2015-07-24 10:23:52 +00:00
|
|
|
|
with lib;
|
|
|
|
|
with types;
|
|
|
|
|
|
|
|
|
|
types // rec {
|
|
|
|
|
|
2015-09-27 13:24:41 +00:00
|
|
|
|
host = submodule ({ config, ... }: {
|
2015-07-24 10:23:52 +00:00
|
|
|
|
options = {
|
|
|
|
|
name = mkOption {
|
|
|
|
|
type = label;
|
2016-02-06 17:54:01 +00:00
|
|
|
|
default = config._module.args.name;
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
|
|
|
|
cores = mkOption {
|
|
|
|
|
type = positive;
|
|
|
|
|
};
|
|
|
|
|
nets = mkOption {
|
|
|
|
|
type = attrsOf net;
|
2016-02-13 15:46:15 +00:00
|
|
|
|
default = {};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
2015-08-13 10:03:59 +00:00
|
|
|
|
|
2016-02-19 15:18:28 +00:00
|
|
|
|
owner = mkOption {
|
|
|
|
|
type = user;
|
|
|
|
|
};
|
|
|
|
|
|
2015-08-13 20:28:21 +00:00
|
|
|
|
extraZones = mkOption {
|
|
|
|
|
default = {};
|
|
|
|
|
# TODO: string is either MX, NS, A or AAAA
|
|
|
|
|
type = with types; attrsOf string;
|
|
|
|
|
};
|
2015-09-26 22:22:50 +00:00
|
|
|
|
|
2015-07-24 16:36:16 +00:00
|
|
|
|
secure = mkOption {
|
|
|
|
|
type = bool;
|
|
|
|
|
default = false;
|
|
|
|
|
description = ''
|
|
|
|
|
If true, then the host is capable of keeping secret information.
|
|
|
|
|
|
|
|
|
|
TODO define minimum requirements for secure hosts
|
|
|
|
|
'';
|
|
|
|
|
};
|
2015-09-27 13:24:41 +00:00
|
|
|
|
|
|
|
|
|
ssh.pubkey = mkOption {
|
2016-03-16 00:54:49 +00:00
|
|
|
|
type = nullOr ssh-pubkey;
|
2015-09-27 13:24:41 +00:00
|
|
|
|
default = null;
|
|
|
|
|
};
|
|
|
|
|
ssh.privkey = mkOption {
|
2016-03-16 00:54:49 +00:00
|
|
|
|
type = nullOr ssh-privkey;
|
2015-09-27 14:15:53 +00:00
|
|
|
|
default = null;
|
2015-09-27 13:24:41 +00:00
|
|
|
|
};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
2015-09-27 13:24:41 +00:00
|
|
|
|
});
|
2015-07-24 10:23:52 +00:00
|
|
|
|
|
|
|
|
|
net = submodule ({ config, ... }: {
|
|
|
|
|
options = {
|
2016-04-08 01:53:34 +00:00
|
|
|
|
name = mkOption {
|
|
|
|
|
type = label;
|
|
|
|
|
default = config._module.args.name;
|
|
|
|
|
};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
via = mkOption {
|
|
|
|
|
type = nullOr net;
|
|
|
|
|
default = null;
|
|
|
|
|
};
|
|
|
|
|
addrs = mkOption {
|
|
|
|
|
type = listOf addr;
|
2016-04-08 01:53:34 +00:00
|
|
|
|
default =
|
|
|
|
|
optional (config.ip4 != null) config.ip4.addr ++
|
|
|
|
|
optional (config.ip6 != null) config.ip6.addr;
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
|
|
|
|
aliases = mkOption {
|
|
|
|
|
# TODO nonEmptyListOf hostname
|
|
|
|
|
type = listOf hostname;
|
2015-09-27 13:24:41 +00:00
|
|
|
|
default = [];
|
|
|
|
|
};
|
2016-04-08 01:53:34 +00:00
|
|
|
|
ip4 = mkOption {
|
|
|
|
|
type = nullOr (submodule {
|
|
|
|
|
options = {
|
|
|
|
|
addr = mkOption {
|
|
|
|
|
type = addr4;
|
|
|
|
|
};
|
|
|
|
|
prefix = mkOption ({
|
|
|
|
|
type = str; # TODO routing prefix (CIDR)
|
|
|
|
|
} // optionalAttrs (config.name == "retiolum") {
|
|
|
|
|
default = "10.243.0.0/16";
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
default = null;
|
|
|
|
|
};
|
|
|
|
|
ip6 = mkOption {
|
|
|
|
|
type = nullOr (submodule {
|
|
|
|
|
options = {
|
|
|
|
|
addr = mkOption {
|
|
|
|
|
type = addr6;
|
|
|
|
|
};
|
|
|
|
|
prefix = mkOption ({
|
|
|
|
|
type = str; # TODO routing prefix (CIDR)
|
|
|
|
|
} // optionalAttrs (config.name == "retiolum") {
|
|
|
|
|
default = "42::/16";
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
default = null;
|
|
|
|
|
};
|
2015-09-27 13:24:41 +00:00
|
|
|
|
ssh = mkOption {
|
|
|
|
|
type = submodule {
|
|
|
|
|
options = {
|
|
|
|
|
port = mkOption {
|
2016-04-17 02:13:32 +00:00
|
|
|
|
type = int;
|
|
|
|
|
default = 22;
|
2015-09-27 13:24:41 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
default = {};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
|
|
|
|
tinc = mkOption {
|
2016-02-06 14:11:30 +00:00
|
|
|
|
type = let net = config; in nullOr (submodule ({ config, ... }: {
|
2015-07-24 10:23:52 +00:00
|
|
|
|
options = {
|
|
|
|
|
config = mkOption {
|
|
|
|
|
type = str;
|
2016-02-06 14:11:30 +00:00
|
|
|
|
default = concatStringsSep "\n" (
|
|
|
|
|
(optionals (net.via != null)
|
2016-07-28 08:55:34 +00:00
|
|
|
|
(map (a: "Address = ${a} ${toString config.port}") net.via.addrs))
|
2016-02-06 14:11:30 +00:00
|
|
|
|
++
|
|
|
|
|
(map (a: "Subnet = ${a}") net.addrs)
|
|
|
|
|
++
|
2016-07-28 11:02:41 +00:00
|
|
|
|
[config.extraConfig]
|
|
|
|
|
++
|
2016-02-06 14:11:30 +00:00
|
|
|
|
[config.pubkey]
|
|
|
|
|
);
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
|
|
|
|
pubkey = mkOption {
|
2016-03-16 01:04:22 +00:00
|
|
|
|
type = tinc-pubkey;
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
2016-07-28 11:02:41 +00:00
|
|
|
|
extraConfig = mkOption {
|
|
|
|
|
description = "Extra Configuration to be appended to the hosts file";
|
|
|
|
|
default = "";
|
|
|
|
|
type = string;
|
|
|
|
|
};
|
2016-07-28 08:55:34 +00:00
|
|
|
|
port = mkOption {
|
|
|
|
|
type = int;
|
|
|
|
|
description = "tinc port to use to connect to host";
|
|
|
|
|
default = 655;
|
|
|
|
|
};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
2015-07-27 00:45:03 +00:00
|
|
|
|
}));
|
|
|
|
|
default = null;
|
2015-07-24 10:23:52 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
positive = mkOptionType {
|
|
|
|
|
name = "positive integer";
|
|
|
|
|
check = x: isInt x && x > 0;
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-06-07 20:36:40 +00:00
|
|
|
|
uint = mkOptionType {
|
|
|
|
|
name = "unsigned integer";
|
|
|
|
|
check = x: isInt x && x >= 0;
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-21 04:27:37 +00:00
|
|
|
|
secret-file = submodule ({ config, ... }: {
|
|
|
|
|
options = {
|
2016-07-03 17:44:41 +00:00
|
|
|
|
name = mkOption {
|
|
|
|
|
type = filename;
|
|
|
|
|
default = config._module.args.name;
|
|
|
|
|
};
|
|
|
|
|
path = mkOption {
|
|
|
|
|
type = absolute-pathname;
|
|
|
|
|
default = "/run/keys/${config.name}";
|
|
|
|
|
};
|
|
|
|
|
mode = mkOption {
|
|
|
|
|
type = file-mode;
|
|
|
|
|
default = "0400";
|
|
|
|
|
};
|
2016-02-21 06:18:13 +00:00
|
|
|
|
owner = mkOption {
|
|
|
|
|
type = user;
|
|
|
|
|
};
|
|
|
|
|
group-name = mkOption {
|
|
|
|
|
type = str;
|
|
|
|
|
default = "root";
|
|
|
|
|
};
|
2016-02-21 04:27:37 +00:00
|
|
|
|
source-path = mkOption {
|
|
|
|
|
type = str;
|
2016-07-03 17:44:41 +00:00
|
|
|
|
default = toString <secrets> + "/${config.name}";
|
2016-02-21 04:27:37 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2016-07-16 19:43:38 +00:00
|
|
|
|
|
|
|
|
|
source = submodule ({ config, ... }: {
|
|
|
|
|
options = {
|
|
|
|
|
type = let
|
|
|
|
|
types = ["file" "git" "symlink"];
|
|
|
|
|
in mkOption {
|
|
|
|
|
type = enum types;
|
|
|
|
|
default = let
|
|
|
|
|
cands = filter (k: config.${k} != null) types;
|
|
|
|
|
in
|
|
|
|
|
if length cands == 1
|
|
|
|
|
then head cands
|
|
|
|
|
else throw "cannot determine type";
|
|
|
|
|
};
|
|
|
|
|
file = let
|
|
|
|
|
file-path = (file-source.getSubOptions "FIXME").path.type;
|
|
|
|
|
in mkOption {
|
|
|
|
|
type = nullOr (either file-source file-path);
|
|
|
|
|
default = null;
|
|
|
|
|
apply = x:
|
|
|
|
|
if file-path.check x
|
|
|
|
|
then { path = x; }
|
|
|
|
|
else x;
|
|
|
|
|
};
|
|
|
|
|
git = mkOption {
|
|
|
|
|
type = nullOr git-source;
|
|
|
|
|
default = null;
|
|
|
|
|
};
|
|
|
|
|
symlink = let
|
|
|
|
|
symlink-target = (symlink-source.getSubOptions "FIXME").target.type;
|
|
|
|
|
in mkOption {
|
|
|
|
|
type = nullOr (either symlink-source symlink-target);
|
|
|
|
|
default = null;
|
|
|
|
|
apply = x:
|
|
|
|
|
if symlink-target.check x
|
|
|
|
|
then { target = x; }
|
|
|
|
|
else x;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
file-source = submodule {
|
|
|
|
|
options = {
|
|
|
|
|
path = mkOption {
|
|
|
|
|
type = absolute-pathname;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
git-source = submodule {
|
|
|
|
|
options = {
|
|
|
|
|
ref = mkOption {
|
|
|
|
|
type = str; # TODO types.git.ref
|
|
|
|
|
};
|
|
|
|
|
url = mkOption {
|
|
|
|
|
type = str; # TODO types.git.url
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
symlink-source = submodule {
|
|
|
|
|
options = {
|
|
|
|
|
target = mkOption {
|
|
|
|
|
type = pathname; # TODO relative-pathname
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2015-10-09 12:07:29 +00:00
|
|
|
|
suffixed-str = suffs:
|
|
|
|
|
mkOptionType {
|
|
|
|
|
name = "string suffixed by ${concatStringsSep ", " suffs}";
|
|
|
|
|
check = x: isString x && any (flip hasSuffix x) suffs;
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-06 17:54:01 +00:00
|
|
|
|
user = submodule ({ config, ... }: {
|
2015-07-24 18:48:00 +00:00
|
|
|
|
options = {
|
2016-02-21 05:38:09 +00:00
|
|
|
|
home = mkOption {
|
|
|
|
|
type = absolute-pathname;
|
|
|
|
|
default = "/home/${config.name}";
|
|
|
|
|
};
|
2015-07-24 23:05:14 +00:00
|
|
|
|
mail = mkOption {
|
|
|
|
|
type = str; # TODO retiolum mail address
|
2016-09-09 17:38:12 +00:00
|
|
|
|
default = "${config._module.args.name}@${config.networking.hostName}.r";
|
2015-07-24 23:05:14 +00:00
|
|
|
|
};
|
2015-07-24 18:48:00 +00:00
|
|
|
|
name = mkOption {
|
2016-02-06 18:37:14 +00:00
|
|
|
|
type = username;
|
2016-02-06 17:54:01 +00:00
|
|
|
|
default = config._module.args.name;
|
2015-07-24 18:48:00 +00:00
|
|
|
|
};
|
2016-03-16 00:57:03 +00:00
|
|
|
|
pgp.pubkeys = mkOption {
|
|
|
|
|
type = attrsOf pgp-pubkey;
|
|
|
|
|
default = {};
|
|
|
|
|
description = ''
|
|
|
|
|
Set of user's PGP public keys.
|
|
|
|
|
|
2016-06-07 00:15:58 +00:00
|
|
|
|
Modules supporting PGP may use well-known key names to define
|
|
|
|
|
default values for options, in which case the well-known name
|
|
|
|
|
should be documented in the respective option's description.
|
2016-03-16 00:57:03 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
2015-07-24 18:48:00 +00:00
|
|
|
|
pubkey = mkOption {
|
2016-03-16 00:54:49 +00:00
|
|
|
|
type = nullOr ssh-pubkey;
|
2016-02-21 06:39:24 +00:00
|
|
|
|
default = null;
|
2015-07-24 18:48:00 +00:00
|
|
|
|
};
|
2016-02-21 05:56:57 +00:00
|
|
|
|
uid = mkOption {
|
|
|
|
|
type = int;
|
|
|
|
|
default = genid config.name;
|
|
|
|
|
};
|
2015-07-24 18:48:00 +00:00
|
|
|
|
};
|
2016-02-06 17:54:01 +00:00
|
|
|
|
});
|
2016-04-26 23:33:39 +00:00
|
|
|
|
group = submodule ({ config, ... }: {
|
|
|
|
|
options = {
|
|
|
|
|
name = mkOption {
|
|
|
|
|
type = username;
|
|
|
|
|
default = config._module.args.name;
|
|
|
|
|
};
|
|
|
|
|
gid = mkOption {
|
|
|
|
|
type = int;
|
|
|
|
|
default = genid config.name;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
});
|
2015-07-24 18:48:00 +00:00
|
|
|
|
|
2016-04-07 18:29:07 +00:00
|
|
|
|
addr = either addr4 addr6;
|
|
|
|
|
addr4 = mkOptionType {
|
|
|
|
|
name = "IPv4 address";
|
|
|
|
|
check = let
|
|
|
|
|
IPv4address = let d = "([1-9]?[0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"; in
|
|
|
|
|
concatMapStringsSep "." (const d) (range 1 4);
|
2016-06-12 23:40:57 +00:00
|
|
|
|
in x: isString x && match IPv4address x != null;
|
2016-04-07 18:29:07 +00:00
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
2016-04-08 02:41:30 +00:00
|
|
|
|
addr6 = mkOptionType {
|
|
|
|
|
name = "IPv6 address";
|
|
|
|
|
check = let
|
|
|
|
|
# TODO check IPv6 address harder
|
|
|
|
|
IPv6address = "[0-9a-f.:]+";
|
2016-06-12 23:40:57 +00:00
|
|
|
|
in x: isString x && match IPv6address x != null;
|
2016-04-08 02:41:30 +00:00
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
2016-03-16 00:57:03 +00:00
|
|
|
|
|
|
|
|
|
pgp-pubkey = str;
|
|
|
|
|
|
2016-03-16 00:54:49 +00:00
|
|
|
|
ssh-pubkey = str;
|
|
|
|
|
ssh-privkey = submodule {
|
|
|
|
|
options = {
|
|
|
|
|
bits = mkOption {
|
|
|
|
|
type = nullOr (enum ["4096"]);
|
|
|
|
|
default = null;
|
|
|
|
|
};
|
|
|
|
|
path = mkOption {
|
|
|
|
|
type = either path str;
|
|
|
|
|
apply = x: {
|
|
|
|
|
path = toString x;
|
|
|
|
|
string = x;
|
|
|
|
|
}.${typeOf x};
|
|
|
|
|
};
|
|
|
|
|
type = mkOption {
|
|
|
|
|
type = enum ["rsa" "ed25519"];
|
|
|
|
|
default = "ed25519";
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
2015-12-28 18:43:31 +00:00
|
|
|
|
|
2016-03-16 01:04:22 +00:00
|
|
|
|
tinc-pubkey = str;
|
|
|
|
|
|
2015-12-28 18:43:31 +00:00
|
|
|
|
krebs.file-location = types.submodule {
|
|
|
|
|
options = {
|
|
|
|
|
# TODO user
|
|
|
|
|
host = mkOption {
|
|
|
|
|
type = host;
|
|
|
|
|
};
|
|
|
|
|
# TODO merge with ssl.privkey.path
|
|
|
|
|
path = mkOption {
|
|
|
|
|
type = types.either types.path types.str;
|
|
|
|
|
apply = x: {
|
|
|
|
|
path = toString x;
|
|
|
|
|
string = x;
|
|
|
|
|
}.${typeOf x};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
2016-02-06 18:37:14 +00:00
|
|
|
|
|
2016-06-12 23:37:51 +00:00
|
|
|
|
file-mode = mkOptionType {
|
|
|
|
|
name = "file mode";
|
|
|
|
|
check = x: isString x && match "[0-7]{4}" x != null;
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-06-04 17:06:50 +00:00
|
|
|
|
haskell.conid = mkOptionType {
|
|
|
|
|
name = "Haskell constructor identifier";
|
|
|
|
|
check = x:
|
|
|
|
|
isString x && match "[[:upper:]][[:lower:]_[:upper:]0-9']*" x != null;
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
haskell.modid = mkOptionType {
|
|
|
|
|
name = "Haskell module identifier";
|
|
|
|
|
check = x: isString x && all haskell.conid.check (splitString "." x);
|
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-07 04:08:32 +00:00
|
|
|
|
# RFC952, B. Lexical grammar, <hname>
|
|
|
|
|
hostname = mkOptionType {
|
|
|
|
|
name = "hostname";
|
2016-06-12 23:40:57 +00:00
|
|
|
|
check = x: isString x && all label.check (splitString "." x);
|
2016-02-07 04:08:32 +00:00
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
# RFC952, B. Lexical grammar, <name>
|
|
|
|
|
# RFC1123, 2.1 Host Names and Numbers
|
|
|
|
|
label = mkOptionType {
|
|
|
|
|
name = "label";
|
|
|
|
|
# TODO case-insensitive labels
|
2016-06-12 23:40:57 +00:00
|
|
|
|
check = x: isString x
|
|
|
|
|
&& match "[0-9A-Za-z]([0-9A-Za-z-]*[0-9A-Za-z])?" x != null;
|
2016-02-07 04:08:32 +00:00
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-06 18:37:14 +00:00
|
|
|
|
# POSIX.1‐2013, 3.278 Portable Filename Character Set
|
|
|
|
|
filename = mkOptionType {
|
|
|
|
|
name = "POSIX filename";
|
2016-06-12 23:40:57 +00:00
|
|
|
|
check = x: isString x && match "([0-9A-Za-z._])[0-9A-Za-z._-]*" x != null;
|
2016-02-06 18:37:14 +00:00
|
|
|
|
merge = mergeOneOption;
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-21 05:38:09 +00:00
|
|
|
|
# POSIX.1‐2013, 3.2 Absolute Pathname
|
|
|
|
|
# TODO normalize slashes
|
|
|
|
|
# TODO two slashes
|
|
|
|
|
absolute-pathname = mkOptionType {
|
|
|
|
|
name = "POSIX absolute pathname";
|
2016-06-12 16:13:05 +00:00
|
|
|
|
check = x: let xs = splitString "/" x; xa = head xs; in
|
2016-07-03 17:41:31 +00:00
|
|
|
|
isString x
|
|
|
|
|
&& stringLength x > 0
|
|
|
|
|
&& (xa == "/" || (xa == "" && all filename.check (tail xs)));
|
2016-06-11 14:36:42 +00:00
|
|
|
|
merge = mergeOneOption;
|
2016-02-21 05:38:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
# POSIX.1‐2013, 3.267 Pathname
|
|
|
|
|
# TODO normalize slashes
|
|
|
|
|
pathname = mkOptionType {
|
|
|
|
|
name = "POSIX pathname";
|
2016-06-12 16:13:05 +00:00
|
|
|
|
check = x: let xs = splitString "/" x; in
|
2016-06-12 23:40:57 +00:00
|
|
|
|
isString x && all filename.check (if head xs == "" then tail xs else xs);
|
2016-06-11 14:36:42 +00:00
|
|
|
|
merge = mergeOneOption;
|
2016-02-21 05:38:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
2016-02-06 18:37:14 +00:00
|
|
|
|
# POSIX.1-2013, 3.431 User Name
|
|
|
|
|
username = mkOptionType {
|
|
|
|
|
name = "POSIX username";
|
2016-06-11 14:29:18 +00:00
|
|
|
|
check = filename.check;
|
2016-06-11 14:36:42 +00:00
|
|
|
|
merge = mergeOneOption;
|
2016-02-06 18:37:14 +00:00
|
|
|
|
};
|
2015-07-24 10:23:52 +00:00
|
|
|
|
}
|