1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.jupyter;
8
9 package = cfg.package;
10
11 kernels = (pkgs.jupyter-kernel.create {
12 definitions = if cfg.kernels != null
13 then cfg.kernels
14 else pkgs.jupyter-kernel.default;
15 });
16
17 notebookConfig = pkgs.writeText "jupyter_config.py" ''
18 ${cfg.notebookConfig}
19
20 c.NotebookApp.password = ${cfg.password}
21 '';
22
23in {
24 meta.maintainers = with maintainers; [ aborsu ];
25
26 options.services.jupyter = {
27 enable = mkEnableOption "Jupyter development server";
28
29 ip = mkOption {
30 type = types.str;
31 default = "localhost";
32 description = ''
33 IP address Jupyter will be listening on.
34 '';
35 };
36
37 package = mkOption {
38 type = types.package;
39 # NOTE: We don't use top-level jupyter because we don't
40 # want to pass in JUPYTER_PATH but use .environment instead,
41 # saving a rebuild.
42 default = pkgs.python3.pkgs.notebook;
43 defaultText = literalExpression "pkgs.python3.pkgs.notebook";
44 description = ''
45 Jupyter package to use.
46 '';
47 };
48
49 command = mkOption {
50 type = types.str;
51 default = "jupyter-notebook";
52 example = "jupyter-lab";
53 description = ''
54 Which command the service runs. Note that not all jupyter packages
55 have all commands, e.g. jupyter-lab isn't present in the default package.
56 '';
57 };
58
59 port = mkOption {
60 type = types.int;
61 default = 8888;
62 description = ''
63 Port number Jupyter will be listening on.
64 '';
65 };
66
67 notebookDir = mkOption {
68 type = types.str;
69 default = "~/";
70 description = ''
71 Root directory for notebooks.
72 '';
73 };
74
75 user = mkOption {
76 type = types.str;
77 default = "jupyter";
78 description = ''
79 Name of the user used to run the jupyter service.
80 For security reason, jupyter should really not be run as root.
81 If not set (jupyter), the service will create a jupyter user with appropriate settings.
82 '';
83 example = "aborsu";
84 };
85
86 group = mkOption {
87 type = types.str;
88 default = "jupyter";
89 description = ''
90 Name of the group used to run the jupyter service.
91 Use this if you want to create a group of users that are able to view the notebook directory's content.
92 '';
93 example = "users";
94 };
95
96 password = mkOption {
97 type = types.str;
98 description = ''
99 Password to use with notebook.
100 Can be generated using:
101 In [1]: from notebook.auth import passwd
102 In [2]: passwd('test')
103 Out[2]: 'sha1:1b961dc713fb:88483270a63e57d18d43cf337e629539de1436ba'
104 NOTE: you need to keep the single quote inside the nix string.
105 Or you can use a python oneliner:
106 "open('/path/secret_file', 'r', encoding='utf8').read().strip()"
107 It will be interpreted at the end of the notebookConfig.
108 '';
109 example = "'sha1:1b961dc713fb:88483270a63e57d18d43cf337e629539de1436ba'";
110 };
111
112 notebookConfig = mkOption {
113 type = types.lines;
114 default = "";
115 description = ''
116 Raw jupyter config.
117 '';
118 };
119
120 kernels = mkOption {
121 type = types.nullOr (types.attrsOf(types.submodule (import ./kernel-options.nix {
122 inherit lib;
123 })));
124
125 default = null;
126 example = literalExpression ''
127 {
128 python3 = let
129 env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
130 ipykernel
131 pandas
132 scikit-learn
133 ]));
134 in {
135 displayName = "Python 3 for machine learning";
136 argv = [
137 "''${env.interpreter}"
138 "-m"
139 "ipykernel_launcher"
140 "-f"
141 "{connection_file}"
142 ];
143 language = "python";
144 logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
145 logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
146 };
147 }
148 '';
149 description = "Declarative kernel config
150
151 Kernels can be declared in any language that supports and has the required
152 dependencies to communicate with a jupyter server.
153 In python's case, it means that ipykernel package must always be included in
154 the list of packages of the targeted environment.
155 ";
156 };
157 };
158
159 config = mkMerge [
160 (mkIf cfg.enable {
161 systemd.services.jupyter = {
162 description = "Jupyter development server";
163
164 after = [ "network.target" ];
165 wantedBy = [ "multi-user.target" ];
166
167 # TODO: Patch notebook so we can explicitly pass in a shell
168 path = [ pkgs.bash ]; # needed for sh in cell magic to work
169
170 environment = {
171 JUPYTER_PATH = toString kernels;
172 };
173
174 serviceConfig = {
175 Restart = "always";
176 ExecStart = ''${package}/bin/${cfg.command} \
177 --no-browser \
178 --ip=${cfg.ip} \
179 --port=${toString cfg.port} --port-retries 0 \
180 --notebook-dir=${cfg.notebookDir} \
181 --NotebookApp.config_file=${notebookConfig}
182 '';
183 User = cfg.user;
184 Group = cfg.group;
185 WorkingDirectory = "~";
186 };
187 };
188 })
189 (mkIf (cfg.enable && (cfg.group == "jupyter")) {
190 users.groups.jupyter = {};
191 })
192 (mkIf (cfg.enable && (cfg.user == "jupyter")) {
193 users.extraUsers.jupyter = {
194 extraGroups = [ cfg.group ];
195 home = "/var/lib/jupyter";
196 createHome = true;
197 useDefaultShell = true; # needed so that the user can start a terminal.
198 };
199 })
200 ];
201}