Skip to content

Commit 3152eff

Browse files
authored
Merge pull request #1399 from drgrice1/asksage-update
Update the `AskSage` method.
2 parents 3d05d1c + c7cdbfc commit 3152eff

9 files changed

Lines changed: 221 additions & 38 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ htdocs/static-assets.json
1010
htdocs/**/*.min.js
1111
htdocs/**/*.min.css
1212
htdocs/tmp
13+
14+
sagecell-docker/sagecell-docker.service

conf/pg_config.dist.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ URLs:
7474
- .
7575
- $pg_root_url/images
7676

77+
# By default the AskSage method uses the public sagecell service located at
78+
# https://sagecell.sagemath.org/service. However, the server at that address
79+
# is no longer accepting public requests. So in order for problems that use
80+
# the AskSage method to work a different sagecell server must be used.
81+
# Uncomment the line below and set the value to a custom sagecell server
82+
# address to use with the AskSage method. If using the docker container built
83+
# with the instructions and files in the sagecell-docker directory in this
84+
# repository, the use the line below as is.
85+
86+
#sagecellService: http://localhost:3500/service
87+
7788
# Flat-file database used to protect against MD5 hash collisions. TeX equations
7889
# are hashed to determine the name of the image file. There is a tiny chance of
7990
# a collision between two TeX strings. This file allows for that. However, this

lib/PGcore.pm

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,8 @@ sub directoryFromPath {
703703
}
704704

705705
sub AskSage {
706-
my $self = shift;
707-
my $python = shift;
708-
my $options = shift;
706+
my ($self, $python, $options) = @_;
707+
$options = {} unless ref $options eq 'HASH';
709708
$options->{curlCommand} = WeBWorK::PG::IO::externalCommand('curl');
710709
WeBWorK::PG::IO::AskSage($python, $options);
711710
}

lib/WeBWorK/PG/IO.pm

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ sub externalCommand {
315315

316316
# Isolate the call to the sage server in case we have to jazz it up.
317317
sub query_sage_server {
318-
my ($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug, $curlCommand) = @_;
318+
my ($python, $url, $setSeed, $webworkfunc, $debug, $curlCommand) = @_;
319319

320320
# Validate url to prevent shell injection — must look like a URL.
321321
if ($url && $url !~ m{^https?://[^\s;|&`\$]+$}) {
@@ -328,7 +328,7 @@ sub query_sage_server {
328328
$curlCommand, '-i',
329329
'-k', '-sS',
330330
'-L', '--data-urlencode',
331-
"accepted_tos=$accepted_tos", '--data-urlencode',
331+
"accepted_tos=true", '--data-urlencode',
332332
'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}', '--data-urlencode',
333333
"code=${setSeed}${webworkfunc}$python", $url // (),
334334
);
@@ -400,8 +400,7 @@ sub query_sage_server {
400400
# Success! Put any extraneous splits back together.
401401
$result = join("\r\n\r\n", @lines);
402402
} else {
403-
warn "ERROR in contacting sage server. Did you accept the terms of service by "
404-
. "setting { accepted_tos => 'true' } in the askSage options?\n$content\n";
403+
warn "ERROR in contacting sage server.\n$content\n";
405404
$result = undef;
406405
}
407406

@@ -421,44 +420,44 @@ sub AskSage {
421420
chomp($python);
422421

423422
# To send values back in a hash, add them to the python WEBWORK dictionary.
424-
my $url = $args->{url} || 'https://sagecell.sagemath.org/service';
425-
my $seed = $args->{seed};
426-
my $accepted_tos = $args->{accepted_tos} || 'false'; # Force author to accept terms of service explicitly.
427-
my $debug = $args->{debug} || 0;
428-
my $setSeed = $seed ? "set_random_seed($seed)\n" : '';
429-
my $curlCommand = $args->{curlCommand};
423+
my $url = $args->{url} || $pg_envir->{URLs}{sagecellService} || 'https://sagecell.sagemath.org/service';
424+
my $seed = $args->{seed};
425+
my $debug = $args->{debug} || 0;
426+
my $setSeed = $seed ? "set_random_seed($seed)\n" : '';
427+
my $curlCommand = $args->{curlCommand};
430428

431429
my $webworkfunc = <<END;
432430
WEBWORK={}
433431
434432
def _webwork_safe_json(o):
435433
import json
436-
def default(o):
437-
try:
438-
if isinstance(o,sage.rings.integer.Integer):
439-
json_obj = int(o)
440-
elif isinstance(o,(sage.rings.real_mpfr.RealLiteral, sage.rings.real_mpfr.RealNumber)):
441-
json_obj = float(o)
442-
elif sage.modules.free_module_element.is_FreeModuleElement(o):
443-
json_obj = list(o)
444-
elif sage.matrix.matrix.is_Matrix(o):
445-
json_obj = [list(i) for i in o.rows()]
446-
elif isinstance(o, SageObject):
447-
json_obj = repr(o)
434+
class WebworkEncoder(json.JSONEncoder):
435+
def default(self, o):
436+
try:
437+
if isinstance(o,sage.rings.integer.Integer):
438+
json_obj = int(o)
439+
elif isinstance(o,(sage.rings.real_mpfr.RealLiteral, sage.rings.real_mpfr.RealNumber)):
440+
json_obj = float(o)
441+
elif isinstance(o, sage.modules.free_module_element.FreeModuleElement):
442+
json_obj = list(o)
443+
elif isinstance(o, sage.matrix.matrix0.Matrix):
444+
json_obj = [list(i) for i in o.rows()]
445+
elif isinstance(o, SageObject):
446+
json_obj = repr(o)
447+
else:
448+
raise TypeError
449+
except TypeError:
450+
pass
448451
else:
449-
raise TypeError
450-
except TypeError:
451-
pass
452-
else:
453-
return json_obj
454-
# Let the base class default method raise the TypeError
455-
return json.JSONEncoder.default(self, o)
456-
return json.dumps(o, default=default)
452+
return json_obj
453+
# Let the base class default method raise the TypeError
454+
return super().default(o)
455+
return json.dumps(o, cls=WebworkEncoder)
457456
END
458457

459458
my $ret = { success => 0 }; # We want to export more than one piece of information.
460459
eval {
461-
my $output = query_sage_server($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug, $curlCommand);
460+
my $output = query_sage_server($python, $url, $setSeed, $webworkfunc, $debug, $curlCommand);
462461

463462
# has something been returned?
464463
not_null($output) or die "Unable to make a sage call to $url.";
@@ -486,7 +485,7 @@ END
486485
warn "now success is $success because status was ok" if $debug;
487486
if ($success) {
488487
my $WEBWORK_variable_non_empty = 0;
489-
my $sage_WEBWORK_data = $decoded->{execute_reply}{user_expressions}{WEBWORK}{data}{'text/plain'};
488+
my $sage_WEBWORK_data = $decoded->{execute_reply}{user_expressions}{WEBWORK}{data}{'text/plain'} // '';
490489
warn "sage_WEBWORK_data $sage_WEBWORK_data" if $debug;
491490
if (not_null($sage_WEBWORK_data)) {
492491
$WEBWORK_variable_non_empty = # another hack because '{}' is sometimes returned

macros/PG.pl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,7 @@ ()
397397
}
398398

399399
sub AskSage {
400-
my $python = shift;
401-
my $options = shift;
402-
WARN_MESSAGE("the second argument to AskSage should be a hash of options") unless $options =~ /HASH/;
400+
my ($python, $options) = @_;
403401
$PG->AskSage($python, $options);
404402
}
405403

sagecell-docker/Dockerfile

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
FROM sagemath/sagemath:10.8
2+
3+
USER root
4+
5+
ENV DEBIAN_FRONTEND=noninteractive
6+
ENV DEBCONF_NONINTERACTIVE_SEEN=true
7+
ENV DEBCONF_NOWARNINGS=yes
8+
9+
RUN apt-get update && apt-get install -y --no-install-recommends \
10+
automake \
11+
build-essential \
12+
curl \
13+
git \
14+
locales \
15+
openssh-server \
16+
python3 \
17+
python-is-python3 \
18+
python3-jupyter-client \
19+
python3-pip \
20+
python3-psutil \
21+
python3-zmq \
22+
rsyslog \
23+
wget \
24+
&& echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \
25+
&& locale-gen \
26+
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
27+
&& apt-get install -y --no-install-recommends --no-install-suggests nodejs
28+
29+
ENV SHELL=/bin/bash
30+
31+
USER sage
32+
33+
RUN echo "Installing SageCell server for SageMath" \
34+
&& sage -pip install lockfile paramiko sockjs-tornado sqlalchemy \
35+
&& git clone --single-branch --branch master --depth 1 https://github.com/sagemath/sagecell.git \
36+
&& cd sagecell \
37+
&& { sage -sh -c make || true; } \
38+
&& sage -sh -c make \
39+
&& rm -rf contrib doc tests .git \
40+
&& cp config_default.py config.py \
41+
&& sed -i 's/"username": None/"username": "sage"/' config.py \
42+
&& sed -i 's/requires_tos = True/requires_tos = False/' config.py
43+
44+
USER root
45+
46+
COPY sagecell-entrypoint.sh /usr/local/bin/sagecell-entrypoint
47+
48+
RUN echo "Configuring internal ssh server" \
49+
&& chmod +x /usr/local/bin/sagecell-entrypoint \
50+
&& echo "ListenAddress 127.0.0.1\nPasswordAuthentication no" \
51+
> /etc/ssh/sshd_config.d/sshd_config.conf
52+
53+
RUN echo "Cleaning the Container" \
54+
&& apt-get clean \
55+
&& rm -rf /var/lib/apt/lists/* \
56+
&& rm -rf /tmp/*
57+
58+
ENV PORT=3500
59+
60+
WORKDIR /home/sage/sagecell
61+
62+
ENTRYPOINT ["sagecell-entrypoint"]
63+
CMD ["sudo", "-H", "-E", "-u", "sage", "sage", "web_server.py", "-p $PORT"]

sagecell-docker/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SageCell Docker Instructions
2+
3+
These are instructions to build and use a docker image for running a SageCell service in production.
4+
5+
## Build and Run the Docker Image
6+
7+
Execute the following command from the `sagecell-docker` directory.
8+
9+
```bash
10+
docker build -t sagecell .
11+
```
12+
13+
Then run the container by executing the following command in the `sagecell-docker` directory.
14+
15+
```bash
16+
docker run --rm -p 127.0.0.1:3500:3500 sagecell:latest
17+
```
18+
19+
Note that the `-p` argument exposes port `3500` inside the container to port `3500` outside the container, but only
20+
allows connections from localhost. This is important as it does not allow anyone else to use the SageCell service other
21+
than the local webwork server. If a different port outside the container is needed, then change the first `3500` to the
22+
desired port. Adding `--rm` removes the created volume for the container when it exits. Exit the container by typing
23+
`Ctrl-C`.
24+
25+
Access by specific external IP addresses is also possible if you want to have the SageCell service on another server.
26+
The easiest way to accomplish this is by using an SSH tunnel. For example, execute
27+
`ssh -L 3500:127.0.0.1:3550 userId@sagecell.server` where `userId` is your username on the SageCell server and
28+
`sagecell.server` is the domain name or IP address of the SageCell server. There are other ways to accomplish this as
29+
well, but the important thing is that access to the SageCell service be highly restricted as it allows untrusted code to
30+
be executed.
31+
32+
## Deploy the Image for Production Usage
33+
34+
Copy the file `sagecell-docker.dist.service` to `sagecell-docker.service` and execute the following command from the
35+
`sagecell-docker` directory to enable a `systemd` service for running the container.
36+
37+
```bash
38+
sudo systemctl enable $(pwd)/sagecell-docker.service
39+
```
40+
41+
Then you can start the container by executing `sudo systemctl start sagecell-docker`, and stop the container by
42+
executing `sudo systemctl stop sagecell-docker`. Note that any time the server is rebooted the service will start
43+
automatically, so you usually do not need to execute those commands other than to start the service the first time after
44+
enabling it.
45+
46+
Note that the service simply executes the command give above for running the container directly. So modify the command
47+
in the `sagecell-docker.service` file as described above if a different port is needed.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[Unit]
2+
Description=sagecell server
3+
After=network.target
4+
5+
[Service]
6+
Type=simple
7+
ExecStart=docker run --rm -p 127.0.0.1:3500:3500 sagecell:latest
8+
KillMode=process
9+
10+
[Install]
11+
WantedBy=multi-user.target
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash
2+
3+
function configure_ssh() {
4+
local key_file=/home/sage/.ssh/id_rsa
5+
if [ $(ssh-add -l &> /dev/null; echo $?) -gt 0 ]; then
6+
# Generate ssh keys for the user.
7+
if [ ! -f $key_file ]; then
8+
[ ! -d /home/sage/.ssh ] && mkdir /home/sage/.ssh
9+
ssh-keygen -t rsa -f $key_file -P "" -q
10+
fi
11+
12+
# Start and configure the ssh-agent.
13+
eval `ssh-agent` > /dev/null
14+
local ssh_fingerprint=$(ssh-keygen -l -f $key_file | awk '{print $2}')
15+
if [ ! -z "$ssh_fingerprint" ] && [ -z "$(ssh-add -l | grep "$ssh_fingerprint")" ]; then
16+
ssh-add $key_file 2> /dev/null
17+
fi
18+
19+
# Add localhost to known_hosts.
20+
[ ! -f /home/sage/.ssh/known_hosts ] && touch /home/sage/.ssh/known_hosts
21+
if [ -z "$(ssh-keygen -F localhost)" ]; then
22+
ssh-keyscan -H localhost > /home/sage/.ssh/known_hosts 2> /dev/null
23+
fi
24+
25+
cat ${key_file}.pub > /home/sage/.ssh/authorized_keys
26+
fi
27+
}
28+
29+
function check_ssh() {
30+
ssh -v -o PreferredAuthentications=publickey -o BatchMode=yes -o ConnectTimeout=10 \
31+
-o StrictHostKeyChecking=no localhost /bin/true &> /dev/null
32+
return $?
33+
}
34+
35+
# Configure and start rsyslog.
36+
sed -i '/imklog/s/^/#/' /etc/rsyslog.conf
37+
rsyslogd
38+
39+
# Start and configure the ssh server for internal use.
40+
service ssh start > /dev/null
41+
42+
export -f configure_ssh
43+
export -f check_ssh
44+
45+
su sage bash -c "configure_ssh"
46+
su sage bash -c "check_ssh"
47+
48+
if [ $? -eq 0 ]; then
49+
echo "Executing: $(eval "echo $@")"
50+
exec $(eval "echo $@")
51+
else
52+
echo "ERROR: SageCell server not started. Failed to configure internal ssh server."
53+
fi

0 commit comments

Comments
 (0)