dcs-package-importer: don’t add a leading / to the index
[debiancodesearch.git] / cmd / dcs-rs-deploy / dcs-rs-deploy.go
blob7f37e0f38a9bf34177ecc2f15f6fcdf41d70077e
1 package main
3 import (
4 "flag"
5 "fmt"
6 "github.com/Debian/dcs/rackspace"
7 "github.com/Debian/dcs/sshutil"
8 "io/ioutil"
9 "log"
10 "strings"
11 "time"
14 var (
15 dcs0ServerId = flag.String("dcs0_server_id",
16 "",
17 "Skip creating a new server and use the specified id as dcs0 vm.")
18 dcsIndexServerIds = flag.String("index_server_ids",
19 "",
20 "comma-separated list of index server IDs.")
21 dcs0VolumeId = flag.String("dcs0_volume_id_dcs",
22 "",
23 "Skip creating a new block storage volume and use the specified id.")
24 dcs0VolumeMirrorId = flag.String("dcs0_volume_id_mirror",
25 "",
26 "Skip creating a new block storage volume and use the specified id.")
28 rs *rackspace.RackspaceClient
31 // All functions defined here directly use log.Fatal() without an OrDie suffix.
33 func attachBlockStorageVolume(serverId, volumeId, deviceNode string) {
34 volume, err := rs.GetVolume(volumeId)
35 if err != nil {
36 log.Fatal(err)
38 volume = volume.BecomeAvailableOrDie(2 * time.Hour)
40 if volume.Status() == "IN-USE" {
41 log.Printf("Volume %s already attached, not attaching.\n", volumeId)
42 return
45 log.Printf("Attaching volume %s to %s\n", volumeId, serverId)
46 attachId, err := rs.AttachBlockStorage(serverId,
47 rackspace.VolumeAttachmentRequest{
48 Device: deviceNode,
49 VolumeId: volumeId,
51 if err != nil {
52 log.Fatal(err)
54 log.Printf("attachment id = %s\n", attachId)
56 volume = volume.BecomeInUseOrDie(30 * time.Minute)
59 // Returns an sshutil.Client because it reconnects on fdisk failures, creating
60 // a new client.
61 func mountBlockStorage(client *sshutil.Client, partition, mountpoint string) *sshutil.Client {
62 // device is “/dev/xvdg” if partition is “/dev/xvdg1”
63 device := partition[:len(partition)-1]
65 // Unmount all mounts within this mountpoint, then the mountpoint itself.
66 grep := `cut -d ' ' -f 2 /proc/mounts | grep '^` + mountpoint + `' | tac`
67 client.RunOrDie(`for mnt in $(` + grep + `); do umount $mnt; done || true`)
69 // Create a partition unless a partition already exists.
70 fdisk := `[ -e ` + partition + ` ] || echo "n\np\n\n\n\nw\n" | fdisk -S 32 -H 32 ` + device
72 // When re-partitioning the disk on which the root filesystem is stored,
73 // the kernel will not re-read the partition table and thus fdisk will
74 // fail.
75 // We reboot the machine and wait until it comes back before continuing.
76 session, err := client.NewSession()
77 if err != nil {
78 log.Fatalf("Failed to create session in SSH connection: %v\n", err)
80 defer session.Close()
81 log.Printf(`[SSH] Running "%s"`, fdisk)
82 err = session.Run(fdisk)
83 if err != nil {
84 log.Printf(`Could not execute SSH command (%v) rebooting\n`, err)
86 client.RunOrDie(`reboot`)
87 // Save host because client may be garbage collected in the loop
88 host := client.Host
89 start := time.Now()
90 for time.Since(start) < 10*time.Minute {
91 time.Sleep(5 * time.Second)
92 log.Printf("Trying to SSH in again…\n")
93 client, err = sshutil.Connect(host)
94 if err != nil {
95 log.Printf("SSH error (retrying): %v\n", err)
96 } else {
97 break
101 // TODO: what has helped at least once is triggering a reboot via the rackspace web interface.
103 if client == nil {
104 log.Fatal("Machine did not come back within 10 minutes after rebooting.")
107 if !client.Successful(`[ -e ` + partition + ` ]`) {
108 log.Fatal("%s not there, even after a reboot!\n", partition)
112 // Create (or overwrite!) an ext4 file system without lazy initialization
113 // (so that the volume is usable immediately) and with SSD-friendly
114 // parameters.
115 // TODO: ^has_journal, barrier=0
116 client.RunOrDie(`mkfs.ext4 -b 4096 -m 0 -E stride=128,stripe-width=128,lazy_itable_init=0,lazy_journal_init=0 ` + partition)
118 client.RunOrDie(fmt.Sprintf("echo %s %s ext4 defaults,nofail,noatime,nodiratime 0 0 >> /etc/fstab", partition, mountpoint))
119 client.RunOrDie(`mkdir -p ` + mountpoint)
120 client.RunOrDie(`mount ` + mountpoint)
122 return client
125 func main() {
126 flag.Parse()
128 var err error
129 rs, err = rackspace.NewClient()
130 if err != nil {
131 log.Fatal("Could not create new rackspace client: %v\n", err)
134 serverId := *dcs0ServerId
135 if serverId == "" {
136 serverId = rs.ServerFromSnapshot("dcs-20g-systemd+dcs+psql",
137 rackspace.CreateServerRequest{
138 Name: "NEW-dcs-0",
139 // This is a 4 GB standard instance
140 FlavorRef: "5",
144 log.Printf("-dcs0_server_id=%s\n", serverId)
146 server, err := rs.GetServer(serverId)
147 if err != nil {
148 log.Fatal(err)
150 server = server.BecomeActiveOrDie(2 * time.Hour)
152 // Attach an SSD block storage volume (unless one was specified)
153 volumeId := *dcs0VolumeId
154 if volumeId == "" {
155 volumeId, err = rs.CreateBlockStorage(
156 rackspace.CreateBlockStorageRequest{
157 DisplayName: "NEW-dcs-src-0",
158 Size: 220,
159 VolumeType: "SSD",
161 if err != nil {
162 log.Fatal(err)
166 // Attach an SSD block storage volume (unless one was specified)
167 volumeMirrorId := *dcs0VolumeMirrorId
168 if volumeMirrorId == "" {
169 volumeMirrorId, err = rs.CreateBlockStorage(
170 rackspace.CreateBlockStorageRequest{
171 DisplayName: "NEW-dcs-mirror-0",
172 // 100 GB is the minimum size. Last time we used 43 GB.
173 Size: 100,
174 VolumeType: "SSD",
176 if err != nil {
177 log.Fatal(err)
181 log.Printf("-dcs0_volume_id_dcs=%s\n", volumeId)
182 log.Printf("-dcs0_volume_id_mirror=%s\n", volumeMirrorId)
184 // We chose xvd[gh] so that it does not clash with xvda, xvdb and
185 // whichever new devices a Rackspace base image might use in the
186 // future :). We use a predictable path because it is easier than
187 // figuring out the path in the subsequent automation.
188 attachBlockStorageVolume(serverId, volumeId, "/dev/xvdg")
189 attachBlockStorageVolume(serverId, volumeMirrorId, "/dev/xvdh")
191 client, err := sshutil.Connect(server.AccessIPv6)
192 if err != nil {
193 log.Fatal(err)
196 // The /dcs/NEW/index.*.idx files are the artifact of the first stage, so
197 // if they are present, skip the first stage entirely.
198 if !client.Successful(`[ -s /dcs/NEW/index.0.idx ]`) {
199 log.Printf("/dcs/NEW/index.0.idx not present, creating new index.\n")
201 // Partition the remaining capacity on the server (≈ 160G) as on-disk
202 // temporary space, since the temporary files exceed the 18G available in
203 // /tmp on our base image.
204 client = mountBlockStorage(client, "/dev/xvda2", "/tmp-disk")
206 // Attach the SSD Block Storage volumes.
207 client = mountBlockStorage(client, "/dev/xvdg1", "/dcs")
208 client = mountBlockStorage(client, "/dev/xvdh1", "/dcs/source-mirror")
210 client.RunOrDie("chown -R dcs.dcs /dcs/source-mirror")
211 client.RunOrDie("chown -R dcs.dcs /dcs/")
212 client.RunOrDie(`chmod 1777 /tmp-disk`)
214 // TODO: timestamps after each step in update-index.sh would be nice
215 client.WriteToFileOrDie("/dcs/update-index.sh", []byte(`#!/bin/sh
216 # Updates the source mirror, generates a new index, verifies it is serving and
217 # then swaps the old index with the new one.
219 # In case anything goes wrong, you can manually swap back the old index, see
220 # swap-index.sh
222 set -e
224 /bin/rm -rf /dcs/NEW /dcs/OLD /dcs/unpacked-new
225 /bin/mkdir /dcs/NEW /dcs/OLD
227 [ -d ~/.gnupg ] || mkdir ~/.gnupg
228 [ -e ~/.gnupg/trustedkeys.gpg ] || cp /usr/share/keyrings/debian-archive-keyring.gpg ~/.gnupg/trustedkeys.gpg
230 GOMAXPROCS=2 /usr/bin/dcs-debmirror -tcp_conns=20 >/tmp/fdm.log 2>&1
231 /usr/bin/debmirror --diff=none --method=http --rsync-extra=none -a none --source -s main -h http.debian.net -r /debian /dcs/source-mirror >/dev/null
232 /usr/bin/debmirror --diff=none --method=http --rsync-extra=none --exclude-deb-section=.* --include golang-mode --nocleanup -a none --arch amd64 -s main -h http.debian.net -r /debian /dcs/source-mirror >/dev/null
234 POPCONDUMP=$(mktemp)
235 if ! wget -q http://udd.debian.org/udd-popcon.sql.xz -O $POPCONDUMP
236 then
237 wget -q http://public-udd-mirror.xvm.mit.edu/snapshots/udd-popcon.sql.xz -O $POPCONDUMP
239 echo 'DROP TABLE popcon; DROP TABLE popcon_src;' | psql udd
240 xz -d -c $POPCONDUMP | psql udd
241 rm $POPCONDUMP
243 /usr/bin/compute-ranking \
244 -mirror_path=/dcs/source-mirror
246 /usr/bin/dcs-unpack \
247 -mirror_path=/dcs/source-mirror \
248 -new_unpacked_path=/dcs/unpacked-new \
249 -old_unpacked_path=/dcs/unpacked >/dev/null
251 /usr/bin/dcs-index \
252 -index_shard_path=/dcs/NEW/ \
253 -unpacked_path=/dcs/unpacked-new/ \
254 -shards 6 >/dev/null
256 [ -d /dcs/unpacked ] && mv /dcs/unpacked /dcs/unpacked-old || true
257 mv /dcs/unpacked-new /dcs/unpacked
259 client.RunOrDie(`chmod +x /dcs/update-index.sh`)
260 client.RunOrDie(`TMPDIR=/tmp-disk nohup su dcs -c "/bin/sh -c 'sh -x /dcs/update-index.sh >/tmp/update.log 2>&1 &'"`)
262 // TODO: i also think we need some sort of lock here. perhaps let systemd run the updater?
265 log.Printf("Now waiting until /dcs/NEW/index.*.idx appear and are > 0 bytes\n")
266 start := time.Now()
267 errors := 0
268 for time.Since(start) < 24*time.Hour {
269 pollclient, err := sshutil.Connect(server.AccessIPv6)
270 if err != nil {
271 log.Printf("Non-fatal polling connection error: %v\n", err)
272 errors++
273 if errors > 30 {
274 log.Fatal("More than 30 connection errors connecting to %s, giving up.\n", server.AccessIPv6)
276 continue
279 // TODO: flag for the number of shards
280 shardsFound := `[ $(find /dcs/NEW/ -iname "index.*.idx" -size +0 -mmin +15 | wc -l) -eq 6 ]`
281 if pollclient.Successful(shardsFound) {
282 log.Printf("All shards present.\n")
283 break
286 time.Sleep(15 * time.Minute)
289 var indexServerIds []string
290 indexServerIds = strings.Split(*dcsIndexServerIds, ",")
291 if *dcsIndexServerIds == "" {
292 // TODO: flag for the number of shards/servers
293 indexServerIds = make([]string, 6)
295 for i := 0; i < len(indexServerIds); i++ {
296 indexServerIds[i] = rs.ServerFromSnapshot("dcs-20g-systemd+dcs",
297 rackspace.CreateServerRequest{
298 Name: fmt.Sprintf("NEW-dcs-index-%d", i),
299 // This is a 2 GB standard instance
300 FlavorRef: "4",
305 log.Printf("-index_server_ids=%s\n", strings.Join(indexServerIds, ","))
307 done := make(chan bool)
308 indexServers := make([]rackspace.Server, len(indexServerIds))
309 for i, _ := range indexServers {
310 go func(i int) {
311 server, err := rs.GetServer(indexServerIds[i])
312 if err != nil {
313 log.Fatal(err)
315 indexServers[i] = server.BecomeActiveOrDie(2 * time.Hour)
316 done <- true
317 }(i)
320 log.Printf("Waiting for all %d index servers to be available…\n",
321 len(indexServerIds))
322 for _ = range indexServers {
323 <-done
326 log.Printf("Index servers available. Copying index…")
328 pubkey, err := ioutil.ReadFile("/home/michael/.ssh/dcs-auto-rs")
329 if err != nil {
330 log.Fatal(err)
332 client.WriteToFileOrDie("~/.ssh/dcs-auto-rs", pubkey)
333 client.RunOrDie("chmod 600 ~/.ssh/dcs-auto-rs")
335 for i, server := range indexServers {
336 go func(i int, server rackspace.Server) {
337 // Create /dcs/
338 indexclient, err := sshutil.Connect(server.AccessIPv6)
339 if err != nil {
340 log.Fatal("Failed to dial: " + err.Error())
342 indexclient.RunOrDie("mkdir -p /dcs/")
343 if indexclient.Successful(fmt.Sprintf("[ -e /dcs/index.%d.idx ]", i)) {
344 log.Printf("Index already present, skipping.\n")
345 done <- true
346 return
348 // “|| true” instead of “rm -f” because globbing fails when there are
349 // no matching files.
350 indexclient.RunOrDie("rm /dcs/index.*.idx || true")
352 client.RunOrDie(
353 fmt.Sprintf("scp -o StrictHostKeyChecking=no -i ~/.ssh/dcs-auto-rs /dcs/NEW/index.%d.idx root@%s:/dcs/",
355 server.PrivateIPv4()))
356 indexclient.RunOrDie(fmt.Sprintf("systemctl restart dcs-index-backend@%d.service", i))
357 indexclient.RunOrDie(fmt.Sprintf("systemctl enable dcs-index-backend@%d.service", i))
358 done <- true
359 }(i, server)
361 log.Printf("Waiting for the index to be copied to all %d index servers…\n",
362 len(indexServerIds))
363 for _ = range indexServers {
364 <-done
366 log.Printf("index copied!\n")
368 backends := []string{}
369 for i, server := range indexServers {
370 backends = append(backends, fmt.Sprintf("%s:%d", server.PrivateIPv4(), 29080+i))
373 // TODO(longterm): configure firewall?
375 client.RunOrDie("mkdir -p /etc/systemd/system/dcs-web.service.d/")
377 client.WriteToFileOrDie(
378 "/etc/systemd/system/dcs-web.service.d/backends.conf",
379 []byte(`[Service]
380 Environment=GOMAXPROCS=2
381 ExecStart=
382 ExecStart=/usr/bin/dcs-web \
383 -template_pattern=/usr/share/dcs/templates/* \
384 -listen_address=`+server.PrivateIPv4()+`:28080 \
385 -use_sources_debian_net=true \
386 -index_backends=`+strings.Join(backends, ",")))
388 client.RunOrDie("systemctl daemon-reload")
389 client.RunOrDie("systemctl enable dcs-web.service")
390 client.RunOrDie("systemctl enable dcs-source-backend.service")
391 client.RunOrDie("systemctl restart dcs-source-backend.service")
392 client.RunOrDie("systemctl restart dcs-web.service")
394 // Install and configure nginx.
395 client.RunOrDie("DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true LC_ALL=C LANGUAGE=C LANG=C apt-get update")
396 client.RunOrDie("DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true LC_ALL=C LANGUAGE=C LANG=C apt-get --force-yes -y install nginx")
397 client.RunOrDie("rm /etc/nginx/sites-enabled/*")
398 client.RunOrDie("mkdir -p /var/cache/nginx/cache")
399 client.RunOrDie("mkdir -p /var/cache/nginx/tmp")
400 client.RunOrDie("chown -R www-data.www-data /var/cache/nginx/")
401 nginxHost, err := ioutil.ReadFile("/home/michael/gocode/src/github.com/Debian/dcs/nginx.example")
402 if err != nil {
403 log.Fatal(err)
405 // dcs-web is listening on the Rackspace ServiceNet (private) IP address.
406 nginxReplaced := strings.Replace(string(nginxHost), "localhost:28080", server.PrivateIPv4()+":28080", -1)
407 client.WriteToFileOrDie("/etc/nginx/sites-available/codesearch", []byte(nginxReplaced))
408 client.RunOrDie("ln -s /etc/nginx/sites-available/codesearch /etc/nginx/sites-enabled/codesearch")
409 client.RunOrDie("systemctl restart nginx.service")
411 // Update DNS
412 domainId, err := rs.GetDomainId("rackspace.zekjur.net")
413 if err != nil {
414 log.Fatal(err)
417 records, err := rs.GetDomainRecords(domainId)
418 if err != nil {
419 log.Fatal(err)
422 var updates []rackspace.Record
423 for _, record := range records {
424 if record.Name == "codesearch.rackspace.zekjur.net" {
425 log.Printf("record %v\n", record)
426 newIp := server.AccessIPv4
427 if record.Type == "AAAA" {
428 newIp = server.AccessIPv6
430 updates = append(updates,
431 rackspace.Record{
432 Id: record.Id,
433 Name: record.Name,
434 Data: newIp,
436 } else if record.Name == "int-dcs-web.rackspace.zekjur.net" {
437 // This record points to the private IPv4 address, used by our
438 // monitoring.
439 log.Printf("record %v\n", record)
440 newIp := server.PrivateIPv4()
441 updates = append(updates,
442 rackspace.Record{
443 Id: record.Id,
444 Name: record.Name,
445 Data: newIp,
447 } else if record.Name == "int-dcs-source-backend.rackspace.zekjur.net" {
448 // This record points to the private IPv4 address, used by our
449 // monitoring.
450 log.Printf("record %v\n", record)
451 newIp := server.PrivateIPv4()
452 updates = append(updates,
453 rackspace.Record{
454 Id: record.Id,
455 Name: record.Name,
456 Data: newIp,
461 if err := rs.UpdateRecords(domainId, updates); err != nil {
462 log.Fatal(err)
465 // TODO: reverse dns for the server
467 log.Printf(`
468 codesearch was deployed to:
469 http://codesearch.rackspace.zekjur.net/
470 http://[%s]/
471 http://%s/
472 `, server.AccessIPv6, server.AccessIPv4)