ec2-provider.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. # |source| this file
  2. #
  3. # Utilities for working with EC2 instances
  4. #
  5. cloud_DefaultZone() {
  6. echo "us-east-1b"
  7. }
  8. cloud_DefaultCustomMemoryGB() {
  9. : # Not implemented
  10. }
  11. cloud_RestartPreemptedInstances() {
  12. : # Not implemented
  13. }
  14. # AWS region is zone with the last character removed
  15. __cloud_GetRegion() {
  16. declare zone="$1"
  17. # AWS region is zone with the last character removed
  18. declare region="${zone:0:$((${#zone} - 1))}"
  19. echo "$region"
  20. }
  21. # Note: sshPrivateKey should be globally defined whenever this function is called.
  22. __cloud_SshPrivateKeyCheck() {
  23. # shellcheck disable=SC2154
  24. if [[ -z $sshPrivateKey ]]; then
  25. echo Error: sshPrivateKey not defined
  26. exit 1
  27. fi
  28. if [[ ! -r $sshPrivateKey ]]; then
  29. echo "Error: file is not readable: $sshPrivateKey"
  30. exit 1
  31. fi
  32. }
  33. #
  34. # __cloud_FindInstances
  35. #
  36. # Find instances with name matching the specified pattern.
  37. #
  38. # For each matching instance, an entry in the `instances` array will be added with the
  39. # following information about the instance:
  40. # "name:public IP:private IP"
  41. #
  42. # filter - The instances to filter on
  43. #
  44. # examples:
  45. # $ __cloud_FindInstances "exact-machine-name"
  46. # $ __cloud_FindInstances "all-machines-with-a-common-machine-prefix*"
  47. #
  48. __cloud_FindInstances() {
  49. declare filter="$1"
  50. instances=()
  51. declare -a regions=("us-east-1" "us-east-2" "us-west-1" "us-west-2" "sa-east-1" "ap-northeast-2" \
  52. "ap-northeast-1" "ap-southeast-2" "ap-southeast-1" "ap-south-1" "eu-west-1" "eu-west-2" "eu-central-1" "ca-central-1")
  53. for region in "${regions[@]}"
  54. do
  55. declare name publicIp privateIp
  56. while read -r name publicIp privateIp zone; do
  57. printf "%-30s | publicIp=%-16s privateIp=%s zone=%s\n" "$name" "$publicIp" "$privateIp" "$zone"
  58. instances+=("$name:$publicIp:$privateIp:$zone")
  59. done < <(aws ec2 describe-instances \
  60. --region "$region" \
  61. --filters \
  62. "Name=tag:name,Values=$filter" \
  63. "Name=instance-state-name,Values=pending,running" \
  64. --query "Reservations[].Instances[].[InstanceId,PublicIpAddress,PrivateIpAddress,Placement.AvailabilityZone]" \
  65. --output text \
  66. )
  67. done
  68. }
  69. #
  70. # cloud_FindInstances [namePrefix]
  71. #
  72. # Find instances with names matching the specified prefix
  73. #
  74. # For each matching instance, an entry in the `instances` array will be added with the
  75. # following information about the instance:
  76. # "name:public IP:private IP"
  77. #
  78. # namePrefix - The instance name prefix to look for
  79. #
  80. # examples:
  81. # $ cloud_FindInstances all-machines-with-a-common-machine-prefix
  82. #
  83. cloud_FindInstances() {
  84. declare namePrefix="$1"
  85. __cloud_FindInstances "$namePrefix*"
  86. }
  87. #
  88. # cloud_FindInstance [name]
  89. #
  90. # Find an instance with a name matching the exact pattern.
  91. #
  92. # For each matching instance, an entry in the `instances` array will be added with the
  93. # following information about the instance:
  94. # "name:public IP:private IP"
  95. #
  96. # name - The instance name to look for
  97. #
  98. # examples:
  99. # $ cloud_FindInstance exact-machine-name
  100. #
  101. cloud_FindInstance() {
  102. declare name="$1"
  103. __cloud_FindInstances "$name"
  104. }
  105. #
  106. # cloud_Initialize [networkName]
  107. #
  108. # Perform one-time initialization that may be required for the given testnet.
  109. #
  110. # networkName - unique name of this testnet
  111. #
  112. # This function will be called before |cloud_CreateInstances|
  113. cloud_Initialize() {
  114. declare networkName="$1"
  115. declare zone="$2"
  116. declare region=
  117. region=$(__cloud_GetRegion "$zone")
  118. __cloud_SshPrivateKeyCheck
  119. aws ec2 delete-key-pair --region "$region" --key-name "$networkName"
  120. aws ec2 import-key-pair --region "$region" --key-name "$networkName" \
  121. --public-key-material file://"${sshPrivateKey}".pub
  122. declare rules
  123. rules=$(cat "$(dirname "${BASH_SOURCE[0]}")"/ec2-security-group-config.json)
  124. aws ec2 delete-security-group --region "$region" --group-name "$networkName" || true
  125. aws ec2 create-security-group --region "$region" --group-name "$networkName" --description "Created automatically by $0"
  126. aws ec2 authorize-security-group-ingress --output table --region "$region" --group-name "$networkName" --cli-input-json "$rules"
  127. }
  128. #
  129. # cloud_CreateInstances [networkName] [namePrefix] [numNodes]
  130. # [machineType] [zone]
  131. # [bootDiskSize] [startupScript] [address]
  132. # [bootDiskType] [additionalDiskSize] [preemptible]
  133. #
  134. # Creates one more identical instances.
  135. #
  136. # networkName - unique name of this testnet
  137. # namePrefix - unique string to prefix all the instance names with
  138. # numNodes - number of instances to create
  139. # machineType - GCE machine type. Note that this may also include an
  140. # `--accelerator=` or other |gcloud compute instances create|
  141. # options
  142. # zone - cloud zone
  143. # bootDiskSize - Optional size of the boot disk in GB
  144. # startupScript - Optional startup script to execute when the instance boots
  145. # address - Optional name of the GCE static IP address to attach to the
  146. # instance. Requires that |numNodes| = 1 and that addressName
  147. # has been provisioned in the GCE region that is hosting `$zone`
  148. # bootDiskType - Optional specify SSD or HDD boot disk
  149. # additionalDiskSize - Optional specify size of additional storage volume
  150. # preemptible - Optionally request a preemptible instance ("true")
  151. #
  152. # Tip: use cloud_FindInstances to locate the instances once this function
  153. # returns
  154. cloud_CreateInstances() {
  155. declare networkName="$1"
  156. declare namePrefix="$2"
  157. declare numNodes="$3"
  158. declare machineType="$4"
  159. declare zone="$5"
  160. declare optionalBootDiskSize="$6"
  161. declare optionalStartupScript="$7"
  162. declare optionalAddress="$8"
  163. declare region=
  164. region=$(__cloud_GetRegion "$zone")
  165. # Select an upstream Ubuntu 18.04 AMI from https://cloud-images.ubuntu.com/locator/ec2/
  166. case $region in
  167. us-east-1)
  168. imageName="ami-0fba9b33b5304d8b4"
  169. ;;
  170. us-east-2)
  171. imageName="ami-0e04554247365d806"
  172. ;;
  173. us-west-1)
  174. imageName="ami-07390b6ff5934a238"
  175. ;;
  176. us-west-2)
  177. imageName="ami-03804ed633fe58109"
  178. ;;
  179. sa-east-1)
  180. imageName="ami-0f1678b6f63a0f923"
  181. ;;
  182. ap-northeast-2)
  183. imageName="ami-0695e34e31339c3ff"
  184. ;;
  185. ap-northeast-1)
  186. imageName="ami-003371bfa26192744"
  187. ;;
  188. ap-southeast-2)
  189. imageName="ami-0401c9e2f645b5557"
  190. ;;
  191. ap-southeast-1)
  192. imageName="ami-08050c889a630f1bd"
  193. ;;
  194. ap-south-1)
  195. imageName="ami-04184c12996409633"
  196. ;;
  197. eu-central-1)
  198. imageName="ami-054e21e355db24124"
  199. ;;
  200. eu-west-1)
  201. imageName="ami-0727f3c2d4b0226d5"
  202. ;;
  203. eu-west-2)
  204. imageName="ami-068f09e337d7da0c4"
  205. ;;
  206. ca-central-1)
  207. imageName="ami-06ed08059bdc08fc9"
  208. ;;
  209. *)
  210. usage "Unsupported region: $region"
  211. ;;
  212. esac
  213. declare -a args
  214. args=(
  215. --key-name "$networkName"
  216. --count "$numNodes"
  217. --region "$region"
  218. --placement "AvailabilityZone=$zone"
  219. --security-groups "$networkName"
  220. --image-id "$imageName"
  221. --instance-type "$machineType"
  222. --tag-specifications "ResourceType=instance,Tags=[{Key=name,Value=$namePrefix}]"
  223. )
  224. if [[ -n $optionalBootDiskSize ]]; then
  225. args+=(
  226. --block-device-mapping "[{\"DeviceName\": \"/dev/sda1\", \"Ebs\": { \"VolumeSize\": $optionalBootDiskSize }}]"
  227. )
  228. fi
  229. if [[ -n $optionalStartupScript ]]; then
  230. args+=(
  231. --user-data "file://$optionalStartupScript"
  232. )
  233. fi
  234. if [[ -n $optionalAddress ]]; then
  235. [[ $numNodes = 1 ]] || {
  236. echo "Error: address may not be supplied when provisioning multiple nodes: $optionalAddress"
  237. exit 1
  238. }
  239. fi
  240. (
  241. set -x
  242. aws ec2 run-instances --output table "${args[@]}"
  243. )
  244. if [[ -n $optionalAddress ]]; then
  245. cloud_FindInstance "$namePrefix"
  246. if [[ ${#instances[@]} -ne 1 ]]; then
  247. echo "Failed to find newly created instance: $namePrefix"
  248. fi
  249. declare instanceId
  250. IFS=: read -r instanceId publicIp privateIp zone < <(echo "${instances[0]}")
  251. (
  252. set -x
  253. # It would be better to poll that the instance has moved to the 'running'
  254. # state instead of blindly sleeping for 30 seconds...
  255. sleep 30
  256. declare region=
  257. region=$(__cloud_GetRegion "$zone")
  258. aws ec2 associate-address \
  259. --instance-id "$instanceId" \
  260. --region "$region" \
  261. --allocation-id "$optionalAddress"
  262. )
  263. fi
  264. }
  265. #
  266. # cloud_DeleteInstances
  267. #
  268. # Deletes all the instances listed in the `instances` array
  269. #
  270. cloud_DeleteInstances() {
  271. if [[ ${#instances[0]} -eq 0 ]]; then
  272. echo No instances to delete
  273. return
  274. fi
  275. # Terminate the instances
  276. for instance in "${instances[@]}"; do
  277. declare name="${instance/:*/}"
  278. declare zone="${instance/*:/}"
  279. declare region=
  280. region=$(__cloud_GetRegion "$zone")
  281. (
  282. set -x
  283. aws ec2 terminate-instances --output table --region "$region" --instance-ids "$name"
  284. )
  285. done
  286. # Wait until the instances are terminated
  287. for instance in "${instances[@]}"; do
  288. declare name="${instance/:*/}"
  289. declare zone="${instance/*:/}"
  290. declare region=
  291. region=$(__cloud_GetRegion "$zone")
  292. while true; do
  293. declare instanceState
  294. instanceState=$(\
  295. aws ec2 describe-instances \
  296. --region "$region" \
  297. --instance-ids "$name" \
  298. --query "Reservations[].Instances[].State.Name" \
  299. --output text \
  300. )
  301. echo "$name: $instanceState"
  302. if [[ $instanceState = terminated ]]; then
  303. break;
  304. fi
  305. sleep 2
  306. done
  307. done
  308. }
  309. #
  310. # cloud_WaitForInstanceReady [instanceName] [instanceIp] [instanceZone] [timeout]
  311. #
  312. # Return once the newly created VM instance is responding. This function is cloud-provider specific.
  313. #
  314. cloud_WaitForInstanceReady() {
  315. declare instanceName="$1"
  316. declare instanceIp="$2"
  317. # declare instanceZone="$3" # unused
  318. declare timeout="$4"
  319. timeout "${timeout}"s bash -c "set -o pipefail; until ping -c 3 $instanceIp | tr - _; do echo .; done"
  320. }
  321. #
  322. # cloud_FetchFile [instanceName] [publicIp] [remoteFile] [localFile]
  323. #
  324. # Fetch a file from the given instance. This function uses a cloud-specific
  325. # mechanism to fetch the file
  326. #
  327. cloud_FetchFile() {
  328. # shellcheck disable=SC2034 # instanceName is unused
  329. declare instanceName="$1"
  330. declare publicIp="$2"
  331. declare remoteFile="$3"
  332. declare localFile="$4"
  333. __cloud_SshPrivateKeyCheck
  334. (
  335. set -x
  336. scp \
  337. -o "StrictHostKeyChecking=no" \
  338. -o "UserKnownHostsFile=/dev/null" \
  339. -o "User=solana" \
  340. -o "IdentityFile=$sshPrivateKey" \
  341. -o "LogLevel=ERROR" \
  342. -F /dev/null \
  343. "solana@$publicIp:$remoteFile" "$localFile"
  344. )
  345. }
  346. #
  347. # cloud_CreateAndAttachPersistentDisk
  348. #
  349. # Not yet implemented for this cloud provider
  350. cloud_CreateAndAttachPersistentDisk() {
  351. echo "ERROR: cloud_CreateAndAttachPersistentDisk is not yet implemented for ec2"
  352. exit 1
  353. }
  354. #
  355. # cloud_StatusAll
  356. #
  357. # Not yet implemented for this cloud provider
  358. cloud_StatusAll() {
  359. echo "ERROR: cloud_StatusAll is not yet implemented for ec2"
  360. }