Skip to content

Commit c7cdbfc

Browse files
committed
Update the AskSage method.
The code that is submitted via the AskSage method no longer works with the newer versions of Sage and needs to be updated. Also the public Sage cell server at https://sagecell.sagemath.org/service no longer accepts public code via its `service` endpoint. So make the URL for a sagecell server configurable. Files for building a sagecell server and deploying it via docker have been added in the `fsagecell-docker` directory. Instructions for doing so are in the `sagecell-docker/README.md` file. Remove the `accepted_tos` parameter from the `AskSage` call. Just add the parameter internally, and stop requiring that the author do so. It is a meaningless gesture to force the author to do so. Furthermore, the docker build is set to not require that parameter. Also make it so that the `AskSage` method can be called without passing any arguments, since there are now none that are required.
1 parent 7445e5c commit c7cdbfc

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)