Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39550
1
#!/usr/bin/env python
2
###############################################################################
3
#
4
# CoCalc: Collaborative Calculation in the Cloud
5
#
6
# Copyright (C) 2016, Sagemath Inc.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#
21
###############################################################################
22
23
24
"""
25
Create a user corresponding to a given project_id.
26
27
create_storage_user.py [project-id]
28
29
You should put the following in visudo:
30
31
salvus ALL=(ALL) NOPASSWD: /usr/local/bin/create_project_user.py *
32
33
"""
34
35
import argparse, hashlib, os, random, time
36
from subprocess import Popen, PIPE
37
38
def uid(uuid):
39
# We take the sha-512 of the uuid just to make it harder to force a collision. Thus even if a
40
# user could somehow generate an account id of their choosing, this wouldn't help them get the
41
# same uid as another user.
42
# 2^32-2=max uid, as keith determined by a program + experimentation.
43
n = hash(hashlib.sha512(uuid).digest()) % (4294967294-1000)
44
return n + 1001
45
46
def cmd(s):
47
out = Popen(s, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=not isinstance(s, list))
48
x = out.stdout.read() + out.stderr.read()
49
e = out.wait()
50
if e:
51
raise RuntimeError(s+x)
52
return x
53
54
def home(project_id):
55
return os.path.join('/projects', project_id)
56
57
def zfs_home_is_mounted(project_id):
58
h = home(project_id)
59
if not os.path.exists(os.path.join(h, '.zfs')):
60
raise RuntimeError("ZFS filesystem %s is not mounted"%h[1:])
61
62
def username(project_id):
63
return project_id.replace('-','')
64
65
def create_user(project_id):
66
"""
67
Create the user the contains the given project data. It is safe to
68
call this function even if the user already exists.
69
"""
70
name = username(project_id)
71
id = uid(project_id)
72
r = open('/etc/passwd').read()
73
i = r.find(name)
74
if i != -1:
75
r = r[i:]
76
i = r.find('\n')
77
u = int(r[:i].split(':')[2])
78
else:
79
u = 0
80
if u == id:
81
# user already exists and has correct id
82
return
83
if u != 0:
84
# there's the username but with wrong id
85
## during migration deleting that user would be a disaster!
86
raise RuntimeError("user %s already exists but with wrong id"%name)
87
#cmd("userdel %s"%name) # this also deletes the group
88
89
# Now make the correct user. The -o makes it so in the incredibly unlikely
90
# event of a collision, no big deal.
91
c = "groupadd -g %s -o %s"%(id, name)
92
for i in range(3):
93
try:
94
cmd(c)
95
break
96
except:
97
time.sleep(random.random())
98
99
# minimal attemp to avoid locking issues
100
c = "useradd -u %s -g %s -o -d %s %s"%(id, id, home(project_id), name)
101
for i in range(3):
102
try:
103
cmd(c)
104
break
105
except:
106
time.sleep(random.random())
107
108
109
# Save account info so it persists through reboots/upgrades/etc. that replaces the ephemeral root fs.
110
if os.path.exists("/mnt/home/etc/"): # UW nodes
111
cmd("cp /etc/passwd /etc/shadow /etc/group /mnt/home/etc/")
112
if os.path.exists("/mnt/conf/etc/"): # GCE nodes
113
cmd("cp /etc/passwd /etc/shadow /etc/group /mnt/conf/etc/")
114
115
def chown_all(project_id):
116
zfs_home_is_mounted(project_id)
117
cmd("zfs set snapdir=hidden %s"%home(project_id).lstrip('/')) # needed for historical reasons
118
id = uid(project_id)
119
cmd('chown %s:%s -R %s'%(id, id, home(project_id)))
120
121
def write_info_json(project_id, host='', base_url=''):
122
zfs_home_is_mounted(project_id)
123
if not host:
124
import socket
125
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
126
s.connect(('10.1.1.1',80))
127
host = s.getsockname()[0]
128
129
path = os.path.join(home(project_id), '.sagemathcloud' + ('-local' if base_url else ''))
130
info_json = os.path.join(path, 'info.json')
131
if not os.path.exists(path):
132
os.makedirs(path)
133
obj = {"project_id":project_id,"location":{"host":host,"username":username(project_id),"port":22,"path":"."},"base_url":base_url}
134
import json
135
open(info_json,'w').write(json.dumps(obj, separators=(',',':')))
136
137
def ensure_ssh_access(project_id):
138
zfs_home_is_mounted(project_id)
139
# If possible, make some attempts to ensure ssh access to this account.
140
h = home(project_id)
141
if not os.path.exists(h):
142
# there is nothing we can possibly do yet -- filesystem not mounted
143
return
144
ssh_path = os.path.join(h, '.ssh')
145
authorized_keys2 = os.path.join(ssh_path, 'authorized_keys2')
146
public_key = open('/home/salvus/.ssh/id_rsa.pub').read().strip()
147
add_public_key = '\n#Added by SageMath Cloud\n' + public_key + '\n'
148
if not os.path.exists(ssh_path):
149
os.makedirs(ssh_path)
150
if not os.path.exists(authorized_keys2):
151
open(authorized_keys2,'w').write(add_public_key)
152
elif public_key not in open(authorized_keys2).read():
153
open(authorized_keys2,'a').write(add_public_key)
154
os.system('chown -R %s. %s'%(username(project_id), ssh_path))
155
os.system('chmod og-rwx -R %s'%ssh_path)
156
157
def killall_user(project_id):
158
u = uid(project_id)
159
os.system("pkill -u %s; sleep 1; pkill -9 -u %s; killall -u %s"%(u,u,username(project_id)))
160
161
def umount_user_home(project_id):
162
os.system("umount %s"%home(project_id))
163
164
def copy_skeleton(project_id):
165
zfs_home_is_mounted(project_id)
166
h = home(project_id)
167
u = username(project_id)
168
if not os.path.exists(h):
169
raise RuntimeError("home directory %s doesn't exist"%h)
170
171
os.system("rsync -axH --update /home/salvus/salvus/salvus/scripts/skel/ %s/"%h) # update so we don't overwrite newer versions
172
# TODO: must fix this -- it could overwrite a user bash or ssh stuff. BAD.
173
cmd("chown -R %s. %s/.sagemathcloud/ %s/.ssh %s/.bashrc"%(u, h, h, h))
174
cmd("chown %s. %s"%(u, h))
175
176
def cgroup(project_id, cpu=1024, memory='8G'):
177
"""
178
Create a cgroup for the given project, and ensure all of the project's processes are in the cgroup.
179
180
INPUT:
181
182
- project_id -- uuid of the project
183
- cpu -- (default: 1024) total number of cpu.shares allocated to this project (across all processes)
184
- memory -- (default: '8G') total amount of RAM allocated to this project (across all processes)
185
"""
186
if not os.path.exists('/sys/fs/cgroup/memory'):
187
188
# do nothing on platforms where cgroups isn't supported (GCE right now, I'm looking at you.)
189
return
190
uname = username(project_id)
191
shares=100000
192
if os.path.exists('/projects/%s/coin'%project_id):
193
shares = 1000
194
if os.path.exists('/projects/%s/minerd'%project_id):
195
shares = 1000
196
if os.path.exists('/projects/%s/sh'%project_id):
197
shares = 1000
198
cmd("cgcreate -g memory,cpu:%s"%uname)
199
cmd('echo "%s" > /sys/fs/cgroup/memory/%s/memory.limit_in_bytes'%(memory, uname))
200
cmd('echo "%s" > /sys/fs/cgroup/cpu/%s/cpu.shares'%(cpu, uname))
201
cmd('echo "%s" > /sys/fs/cgroup/cpu/%s/cpu.cfs_quota_us'%(shares, uname))
202
z = "\n%s cpu,memory %s\n"%(uname, uname)
203
cur = open("/etc/cgrules.conf").read()
204
if z not in cur:
205
open("/etc/cgrules.conf",'a').write(z)
206
cmd('service cgred restart')
207
try:
208
pids = cmd("ps -o pid -u %s"%uname).split()[1:]
209
except RuntimeError:
210
# ps returns an error code if there are NO processes at all (a common condition).
211
pids = []
212
if pids:
213
try:
214
cmd("cgclassify %s"%(' '.join(pids)))
215
# ignore cgclassify errors, since processes come and go, etc.
216
except RuntimeError:
217
pass
218
219
if __name__ == "__main__":
220
parser = argparse.ArgumentParser(description="Project user control script")
221
parser.add_argument("--kill", help="kill all processes owned by the user", default=False, action="store_const", const=True)
222
parser.add_argument("--umount", help="run umount on the project's home as root", default=False, action="store_const", const=True)
223
parser.add_argument("--skel", help="rsync /home/salvus/salvus/salvus/scripts/skel/ to the home directory of the project", default=False, action="store_const", const=True)
224
parser.add_argument("--create", help="create the project user", default=False, action="store_const", const=True)
225
parser.add_argument("--base_url", help="the base url (default:'')", default="", type=str)
226
parser.add_argument("--host", help="the host ip address on the tinc vpn (default: auto-detect)", default="", type=str)
227
parser.add_argument("--chown", help="chown all the files in /projects/projectid", default=False, action="store_const", const=True)
228
parser.add_argument("--cgroup", help="put project in given control group (format: --cgroup=cpu:1024,memory:10G)", default="", type=str)
229
parser.add_argument("project_id", help="the uuid of the project", type=str)
230
args = parser.parse_args()
231
if args.create:
232
create_user(args.project_id)
233
write_info_json(project_id=args.project_id, host=args.host, base_url=args.base_url)
234
ensure_ssh_access(args.project_id)
235
if args.skel:
236
copy_skeleton(args.project_id)
237
if args.kill:
238
killall_user(args.project_id)
239
if args.umount:
240
umount_user_home(args.project_id)
241
if args.chown:
242
chown_all(args.project_id)
243
if args.cgroup:
244
kwds = dict([x.split(':') for x in args.cgroup.split(',')])
245
cgroup(args.project_id, **kwds)
246
247
248
249