Add Licensing and Contributing
[candle] / src / main / aws / aws.lisp
1 ; Copyright 2022 Frank Duncan (frank@consxy.com) under AGPL3.  See distributed LICENSE.txt.
2 (in-package #:candle-aws)
3
4 (defvar *aws-mutex* (sb-thread:make-mutex))
5 (defvar *aws-waitq* (sb-thread:make-waitqueue))
6 (defvar *aws-state* :initial)
7 (defvar *aws-exec*)
8 (defvar *aws-instance-id*)
9 (defvar *aws-username*)
10 (defvar *aws-keyfile*)
11 (defvar *rsync-exec*)
12 (defvar *ssh-exec*)
13 (defvar *remote-work-dir*)
14 (defvar *remote-candle-location*)
15
16 (defmethod candle:process-job-in-system ((job-system (eql :aws)) job)
17  (sb-thread:with-mutex (*aws-mutex*)
18   ; Don't start it up until we process the first job
19   (when (eql :initial *aws-state*)
20    (setf *aws-state* :down)
21    (start-shutdown-thread))
22   (when (eql :down *aws-state*) (start-aws-box))
23   (let
24    ((retn (multiple-value-list (run-job job))))
25    (setf *aws-state* :up)
26    (sb-thread:condition-broadcast *aws-waitq*)
27    (values-list retn))))
28
29 (defun start-shutdown-thread ()
30  (log:info "Starting AWS shutdown thread")
31  (sb-thread:make-thread
32   (lambda ()
33    (loop
34     (sb-thread:with-mutex (*aws-mutex*)
35      (when (eql :down *aws-state*)
36       (sb-thread:condition-wait *aws-waitq* *aws-mutex*))
37      (when (eql :shutting-down-soon *aws-state*)
38       (stop-aws-box)
39       (setf *aws-state* :down))
40      (when (eql :up *aws-state*)
41       (setf *aws-state* :shutting-down-soon)))
42     (sleep 30)))
43   :name "AWS Shutdown Thread"))
44
45 (defmethod candle:shutdown-system ((job-system (eql :aws)))
46  (log:info "Shutting down AWS box for exit")
47  ; If there's a job going, we need to wait for it to finish
48  (sb-thread:with-mutex (*aws-mutex*))
49  (stop-aws-box))
50
51 (defun aws-command (cmd &rest args)
52  (with-output-to-string (out)
53   (sb-ext:run-program
54    *aws-exec*
55    (append
56     (list "ec2" cmd)
57     args)
58    :output out
59    :error *error-output*)))
60
61 (defun describe-property (property)
62  (read-from-string
63   (aws-command
64    "describe-instances"
65    "--instance-ids"
66    *aws-instance-id*
67    "--query"
68    (format nil "Reservations[0].Instances[0].~A" property))))
69
70 (defun get-remote-state ()
71  (intern (string-upcase (describe-property "State.Name")) :keyword))
72
73 (defun start-aws-box ()
74  (aws-command "start-instances" "--instance-ids" *aws-instance-id*)
75  (loop
76   :repeat 8
77   :until (eql :running (get-remote-state))
78   :do (sleep 15))
79  ; Make sure ssh and services are started up
80  (sleep 15)
81  (when (not (eql :running (get-remote-state)))
82   (error "Waited two minutes and still not running...?")))
83
84 (defun stop-aws-box ()
85  (aws-command "stop-instances" "--instance-ids" *aws-instance-id*)
86  (loop
87   :repeat 8
88   :until (eql :stopped (get-remote-state))
89   :do (sleep 15))
90  (when (not (eql :stopped (get-remote-state)))
91   (error "Waited two minutes and still not stopped...?")))
92
93 (defun run-job (job)
94  (sb-ext:run-program
95   *rsync-exec*
96   (list
97    "-az"
98    "--delete"
99    "-e"
100    (format nil "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~A" *aws-keyfile*)
101    "--exclude"
102    ".git"
103    (candle:project-dir (candle:job-project job))
104    (format nil "~A@~A:~A/~A" *aws-username*
105     (describe-property "PublicIpAddress")
106     *remote-work-dir*
107     (candle:project-name (candle:job-project job)))))
108  (let*
109   ((out nil)
110    (code nil))
111   (setf out
112    (with-output-to-string (out-str)
113     (setf code
114      (sb-ext:process-exit-code
115       (sb-ext:run-program
116        *ssh-exec*
117        (list
118         "-o"
119         "StrictHostKeyChecking=no"
120         "-o"
121         "UserKnownHostsFile=/dev/null"
122         "-i"
123         *aws-keyfile*
124         (describe-property "PublicIpAddress")
125         (format nil "cd ~A/~A ; ~A run --env aws"
126          *remote-work-dir*
127          (candle:project-name (candle:job-project job))
128          *remote-candle-location*))
129        :output out-str
130        :error out-str
131        :wait t)))))
132   (values (zerop code) out)))