devbox_launcher 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +17 -18
- data/README.md +7 -8
- data/devbox_launcher.gemspec +1 -1
- data/lib/devbox_launcher.rb +3 -0
- data/lib/devbox_launcher/box.rb +214 -0
- data/lib/devbox_launcher/cli.rb +1 -175
- data/lib/devbox_launcher/models/box.rb +224 -0
- data/lib/devbox_launcher/models/mutagen.rb +23 -0
- data/lib/devbox_launcher/version.rb +1 -1
- metadata +12 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d035caddf255e1498c26b8695f9b1bc538f5e10eebadced13a96854570160627
|
4
|
+
data.tar.gz: a8a84b35b07deed752c2d955a0bfa7a7e54c0c1999e8aaa56be3e71437da6733
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee9fabcc46289524f0f6ebdfc1c1725c2e2530bc763e802661a54773ad071bbd225614f1835aab75b3bfef9dbc3c68c6567d396cd3286693319501e109b1134a
|
7
|
+
data.tar.gz: 441a937f1606f125f4c18125ac8bc646df62d052dee14fdeb570a7dfcb2d7e56805d5c601e1940a735fd4b448434bbf925fe7b3a571cee1ed7bb38b534a68b76
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [0.5.0] - 2021-04-06
|
8
|
+
### Added
|
9
|
+
- Ability to specify `zone` in config
|
10
|
+
|
11
|
+
## [0.4.0]
|
12
|
+
### Added
|
13
|
+
- Sync mutagen with two-way-resolved
|
14
|
+
|
15
|
+
## [0.3.5]
|
16
|
+
### Fixed
|
17
|
+
- When running commands, also rescue from whitelist of exceptions, and retry
|
18
|
+
|
19
|
+
## [0.3.4]
|
20
|
+
### Added
|
21
|
+
- Rescue from `Errno::ECONNREFUSED` and retry
|
22
|
+
|
23
|
+
## [0.3.3]
|
24
|
+
### Fixed
|
25
|
+
- Support launching multiple boxes at the same time
|
26
|
+
|
7
27
|
## [0.3.2]
|
8
28
|
### Fixed
|
9
29
|
- Fix: add missing file
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
devbox_launcher (0.
|
4
|
+
devbox_launcher (0.5.0)
|
5
5
|
activesupport (~> 6.0)
|
6
6
|
bcrypt_pbkdf (~> 1.0)
|
7
7
|
ed25519 (~> 1.2)
|
@@ -14,31 +14,31 @@ PATH
|
|
14
14
|
GEM
|
15
15
|
remote: https://rubygems.org/
|
16
16
|
specs:
|
17
|
-
activesupport (6.
|
17
|
+
activesupport (6.1.3)
|
18
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
19
|
-
i18n (>=
|
20
|
-
minitest (
|
21
|
-
tzinfo (~>
|
22
|
-
zeitwerk (~> 2.
|
23
|
-
bcrypt_pbkdf (1.0
|
19
|
+
i18n (>= 1.6, < 2)
|
20
|
+
minitest (>= 5.1)
|
21
|
+
tzinfo (~> 2.0)
|
22
|
+
zeitwerk (~> 2.3)
|
23
|
+
bcrypt_pbkdf (1.1.0)
|
24
24
|
byebug (11.0.1)
|
25
25
|
coderay (1.1.2)
|
26
|
-
concurrent-ruby (1.1.
|
26
|
+
concurrent-ruby (1.1.8)
|
27
27
|
diff-lcs (1.3)
|
28
28
|
ed25519 (1.2.4)
|
29
|
-
i18n (1.8.
|
29
|
+
i18n (1.8.9)
|
30
30
|
concurrent-ruby (~> 1.0)
|
31
31
|
method_source (0.9.2)
|
32
|
-
minitest (5.14.
|
32
|
+
minitest (5.14.4)
|
33
33
|
net-ssh (5.2.0)
|
34
|
-
os (1.1.
|
34
|
+
os (1.1.1)
|
35
35
|
pry (0.12.2)
|
36
36
|
coderay (~> 1.1.0)
|
37
37
|
method_source (~> 0.9.0)
|
38
38
|
pry-byebug (3.7.0)
|
39
39
|
byebug (~> 11.0)
|
40
40
|
pry (~> 0.10)
|
41
|
-
rake (
|
41
|
+
rake (13.0.1)
|
42
42
|
rspec (3.9.0)
|
43
43
|
rspec-core (~> 3.9.0)
|
44
44
|
rspec-expectations (~> 3.9.0)
|
@@ -54,11 +54,10 @@ GEM
|
|
54
54
|
rspec-support (3.9.0)
|
55
55
|
ruby-watchman (0.0.2)
|
56
56
|
ssh-config (0.1.3)
|
57
|
-
thor (1.0
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
zeitwerk (2.3.0)
|
57
|
+
thor (1.1.0)
|
58
|
+
tzinfo (2.0.4)
|
59
|
+
concurrent-ruby (~> 1.0)
|
60
|
+
zeitwerk (2.4.2)
|
62
61
|
|
63
62
|
PLATFORMS
|
64
63
|
ruby
|
@@ -67,7 +66,7 @@ DEPENDENCIES
|
|
67
66
|
bundler (~> 2.0)
|
68
67
|
devbox_launcher!
|
69
68
|
pry-byebug
|
70
|
-
rake (~>
|
69
|
+
rake (~> 13.0)
|
71
70
|
rspec (~> 3.0)
|
72
71
|
|
73
72
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -4,13 +4,9 @@ Start devboxes quickly
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
Install the gem:
|
8
|
-
|
9
|
-
|
10
|
-
gem install devbox_launcher
|
11
|
-
```
|
12
|
-
|
13
|
-
Setup gcloud init with the project that contains your VM.
|
7
|
+
- Install the gem: `gem install devbox_launcher`
|
8
|
+
- Setup `gcloud init` with the project that contains your VM
|
9
|
+
- Install [mutagen](https://mutagen.io)
|
14
10
|
|
15
11
|
## Usage
|
16
12
|
|
@@ -21,6 +17,9 @@ Create the config file at `~/.devbox_launcher.yml` so you type less. This is an
|
|
21
17
|
```yml
|
22
18
|
ramon@email.com:
|
23
19
|
project: general-192303
|
20
|
+
# zone not necessarily required, but sometimes starting the box
|
21
|
+
# fails without this:
|
22
|
+
zone: us-central1-a
|
24
23
|
box: your-instance-name
|
25
24
|
mutagen:
|
26
25
|
alpha: /mnt/c/Users/me/src # local machine
|
@@ -36,7 +35,7 @@ To start and create the mutagen session:
|
|
36
35
|
devbox start your-username
|
37
36
|
```
|
38
37
|
|
39
|
-
If you want to mosh in immediately, add the `--mosh` switch.
|
38
|
+
If you want to mosh in immediately, add the `--mosh` switch. Yes, mosh needs to be [installed](https://mosh.org/) in your development machine.
|
40
39
|
|
41
40
|
Note: Linux users that sync mutagen sessions need to install [Watchman](https://facebook.github.io/watchman/).
|
42
41
|
|
data/devbox_launcher.gemspec
CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.require_paths = ["lib"]
|
31
31
|
|
32
32
|
spec.add_development_dependency "bundler", "~> 2.0"
|
33
|
-
spec.add_development_dependency "rake", "~>
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
34
34
|
spec.add_development_dependency "rspec", "~> 3.0"
|
35
35
|
|
36
36
|
spec.add_runtime_dependency "thor", "~> 1.0"
|
data/lib/devbox_launcher.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "active_support/core_ext/hash/indifferent_access"
|
2
|
+
require "active_support/core_ext/object/blank"
|
2
3
|
require "ssh-config"
|
3
4
|
require "open3"
|
4
5
|
require "thor"
|
@@ -17,3 +18,5 @@ end
|
|
17
18
|
require "devbox_launcher/cli"
|
18
19
|
require "devbox_launcher/watchman"
|
19
20
|
require "devbox_launcher/models/description"
|
21
|
+
require "devbox_launcher/models/mutagen"
|
22
|
+
require "devbox_launcher/models/box"
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module DevboxLauncher
|
2
|
+
class Box
|
3
|
+
|
4
|
+
WAIT_BOOT_RESCUED_EXCEPTIONS = [
|
5
|
+
Net::SSH::ConnectionTimeout,
|
6
|
+
Net::SSH::Disconnect,
|
7
|
+
Errno::ECONNRESET,
|
8
|
+
Errno::ETIMEDOUT,
|
9
|
+
Errno::ECONNREFUSED,
|
10
|
+
]
|
11
|
+
WAIT_BOOT_IN_SECONDS = 10.freeze
|
12
|
+
DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
|
13
|
+
SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
|
14
|
+
CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
|
15
|
+
CONFIG = YAML.load_file(CONFIG_PATH).freeze
|
16
|
+
|
17
|
+
attr_reader :account
|
18
|
+
|
19
|
+
def initialize(account)
|
20
|
+
@account = account
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
start_stdout, start_stderr, start_status =
|
25
|
+
run_command(start_cmd)
|
26
|
+
|
27
|
+
set_ssh_config!(hostname, {
|
28
|
+
username: username,
|
29
|
+
ip: description.ip,
|
30
|
+
})
|
31
|
+
|
32
|
+
wait_boot
|
33
|
+
|
34
|
+
reset_mutagen_session(
|
35
|
+
mutagen_config: config[:mutagen],
|
36
|
+
hostname: hostname,
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def start_cmd
|
41
|
+
args = {
|
42
|
+
project: config[:project],
|
43
|
+
account: account,
|
44
|
+
}.map do |(key, val)|
|
45
|
+
["--#{key}", val].join("=")
|
46
|
+
end.join(" ")
|
47
|
+
|
48
|
+
[
|
49
|
+
"gcloud",
|
50
|
+
"compute",
|
51
|
+
"instances",
|
52
|
+
"start",
|
53
|
+
name,
|
54
|
+
args
|
55
|
+
].join(" ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait_boot(tries: 1)
|
59
|
+
Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
|
60
|
+
puts "[#{ssh.exec!('date').chomp}] Machine booted"
|
61
|
+
end
|
62
|
+
rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
|
63
|
+
puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
|
64
|
+
|
65
|
+
sleep WAIT_BOOT_IN_SECONDS
|
66
|
+
|
67
|
+
description = describe(name)
|
68
|
+
if !description.running?
|
69
|
+
puts "Detected that the machine is not running " \
|
70
|
+
"(status is #{description.status}). Booting it..."
|
71
|
+
start_box name, username
|
72
|
+
end
|
73
|
+
|
74
|
+
wait_boot name, username, tries: tries+1
|
75
|
+
end
|
76
|
+
|
77
|
+
def description(reload: false)
|
78
|
+
return @description if !reload && @description
|
79
|
+
|
80
|
+
puts "Fetching box's description..."
|
81
|
+
|
82
|
+
describe_command = %Q(gcloud compute instances describe #{name})
|
83
|
+
describe_stdout, describe_stderr, describe_status =
|
84
|
+
run_command(describe_command)
|
85
|
+
|
86
|
+
if !describe_status.success?
|
87
|
+
msg = "Problem fetching the description of #{name}. "
|
88
|
+
msg += "Please ensure you can call `#{describe_command}`.\n"
|
89
|
+
msg += "Error:\n"
|
90
|
+
msg += describe_stderr
|
91
|
+
fail msg
|
92
|
+
end
|
93
|
+
|
94
|
+
@description = Description.new(describe_stdout)
|
95
|
+
end
|
96
|
+
|
97
|
+
def set_ssh_config!(hostname, username:, ip:)
|
98
|
+
FileUtils.touch(SSH_CONFIG_PATH)
|
99
|
+
config = ConfigFile.new
|
100
|
+
args = {
|
101
|
+
"HostName" => ip,
|
102
|
+
"User" => username,
|
103
|
+
"IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
|
104
|
+
}
|
105
|
+
args.each do |key, value|
|
106
|
+
config.set(hostname, key, value)
|
107
|
+
end
|
108
|
+
config.save
|
109
|
+
end
|
110
|
+
|
111
|
+
def reset_mutagen_session
|
112
|
+
mutagen_config = config[:mutagen]
|
113
|
+
return if mutagen_config.nil?
|
114
|
+
|
115
|
+
alpha_dir = mutagen_config[:alpha]
|
116
|
+
beta_dir = mutagen_config[:beta]
|
117
|
+
|
118
|
+
return if alpha_dir.nil? || beta_dir.nil?
|
119
|
+
|
120
|
+
terminate_mutagen_session
|
121
|
+
|
122
|
+
create_mutagen_session(
|
123
|
+
alpha_dir: alpha_dir,
|
124
|
+
beta_dir: beta_dir,
|
125
|
+
hostname: hostname,
|
126
|
+
username: username,
|
127
|
+
)
|
128
|
+
|
129
|
+
if OS.linux?
|
130
|
+
watch_alpha(alpha_dir: alpha_dir, hostname: hostname)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def terminate_mutagen_session(username)
|
135
|
+
puts "Terminating mutagen session..."
|
136
|
+
terminate_mutagen_command =
|
137
|
+
%Q(mutagen terminate --label-selector=#{username})
|
138
|
+
terminate_mutagen_stdout,
|
139
|
+
terminate_mutagen_stderr,
|
140
|
+
terminate_mutagen_status =
|
141
|
+
run_command(terminate_mutagen_command)
|
142
|
+
|
143
|
+
if not terminate_mutagen_status.success?
|
144
|
+
# mutagen prints to stdout and stderr
|
145
|
+
msg = "Failed to terminate mutagen sessions: " \
|
146
|
+
"#{terminate_mutagen_stdout} -" \
|
147
|
+
"#{terminate_mutagen_stderr}"
|
148
|
+
fail msg
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_mutagen_session(alpha_dir:, beta_dir:, hostname:, username:)
|
153
|
+
puts "Create mutagen session syncing local #{alpha_dir} " \
|
154
|
+
"with #{hostname} #{beta_dir}"
|
155
|
+
|
156
|
+
create_mutagen_command = [
|
157
|
+
"mutagen sync create",
|
158
|
+
alpha_dir,
|
159
|
+
"#{hostname}:#{beta_dir}",
|
160
|
+
"--label=#{username}",
|
161
|
+
"--sync-mode=two-way-resolved",
|
162
|
+
]
|
163
|
+
create_mutagen_command << "--watch-mode-alpha=no-watch" if OS.linux?
|
164
|
+
|
165
|
+
create_mutagen_stdout,
|
166
|
+
create_mutagen_stderr,
|
167
|
+
create_mutagen_status =
|
168
|
+
run_command(create_mutagen_command.join(" "))
|
169
|
+
|
170
|
+
if not create_mutagen_status.success?
|
171
|
+
# mutagen prints to stdout and stderr
|
172
|
+
msg = "Failed to create mutagen sessions: " \
|
173
|
+
"#{create_mutagen_stdout} -" \
|
174
|
+
"#{create_mutagen_stderr}"
|
175
|
+
fail msg
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def watch_alpha(alpha_dir:, hostname:)
|
180
|
+
watchman = Watchman.new(dir: alpha_dir)
|
181
|
+
watchman.trigger("mutagen sync flush --label-selector=#{hostname}")
|
182
|
+
end
|
183
|
+
|
184
|
+
def name
|
185
|
+
@name ||= config[:box]
|
186
|
+
end
|
187
|
+
|
188
|
+
def hostname
|
189
|
+
[name, username, "devbox"].join("-")
|
190
|
+
end
|
191
|
+
|
192
|
+
def username
|
193
|
+
@username ||= account.gsub(/\W/, "_")
|
194
|
+
end
|
195
|
+
|
196
|
+
def config
|
197
|
+
return @config if @config
|
198
|
+
|
199
|
+
if not CONFIG.has_key?(account)
|
200
|
+
fail "No config in #{CONFIG_PATH} found for #{account}"
|
201
|
+
end
|
202
|
+
|
203
|
+
@config = CONFIG[account].with_indifferent_access
|
204
|
+
end
|
205
|
+
|
206
|
+
def run_command(command, tries: 0)
|
207
|
+
Open3.capture3(command)
|
208
|
+
rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
|
209
|
+
sleep WAIT_BOOT_IN_SECONDS
|
210
|
+
run_command(command, tries+1)
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|
data/lib/devbox_launcher/cli.rb
CHANGED
@@ -6,186 +6,12 @@ module DevboxLauncher
|
|
6
6
|
SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
|
7
7
|
CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
|
8
8
|
CONFIG = YAML.load_file(CONFIG_PATH).freeze
|
9
|
-
LABEL = "devbox".freeze
|
10
9
|
|
11
10
|
desc "start configured box for account", "Start a devbox by account"
|
12
11
|
option :mosh, type: :boolean, desc: "Mosh in"
|
13
12
|
|
14
13
|
def start(account)
|
15
|
-
|
16
|
-
fail "No config in #{CONFIG_PATH} found for #{account}"
|
17
|
-
end
|
18
|
-
|
19
|
-
config = CONFIG[account].with_indifferent_access
|
20
|
-
name = config[:box]
|
21
|
-
|
22
|
-
username = account.gsub(/\W/, "_")
|
23
|
-
|
24
|
-
puts "Starting #{name}..."
|
25
|
-
|
26
|
-
set_account_command = %Q(gcloud config set account #{account})
|
27
|
-
set_account_stdout, set_account_stderr, set_account_status =
|
28
|
-
Open3.capture3(set_account_command)
|
29
|
-
|
30
|
-
set_project_command = %Q(gcloud config set project #{config[:project]})
|
31
|
-
set_project_stdout, set_project_stderr, set_project_status =
|
32
|
-
Open3.capture3(set_project_command)
|
33
|
-
|
34
|
-
start_box name, username
|
35
|
-
|
36
|
-
wait_boot(name, username)
|
37
|
-
|
38
|
-
hostname = hostname_for(name)
|
39
|
-
|
40
|
-
reset_mutagen_session(
|
41
|
-
mutagen_config: config[:mutagen],
|
42
|
-
hostname: hostname,
|
43
|
-
)
|
44
|
-
|
45
|
-
if options[:mosh]
|
46
|
-
mosh_command = %Q(mosh #{hostname})
|
47
|
-
system(mosh_command)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
no_commands do
|
52
|
-
def wait_boot(name, username, tries: 1)
|
53
|
-
hostname = hostname_for(name)
|
54
|
-
|
55
|
-
Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
|
56
|
-
puts "[#{ssh.exec!('date').chomp}] Machine booted"
|
57
|
-
end
|
58
|
-
rescue Net::SSH::ConnectionTimeout, Net::SSH::Disconnect, Errno::ECONNRESET
|
59
|
-
puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
|
60
|
-
|
61
|
-
sleep WAIT_BOOT_IN_SECONDS
|
62
|
-
|
63
|
-
description = describe(name)
|
64
|
-
if !description.running?
|
65
|
-
puts "Detected that the machine is not running " \
|
66
|
-
"(status is #{description.status}). Booting it..."
|
67
|
-
start_box name, username
|
68
|
-
end
|
69
|
-
|
70
|
-
wait_boot name, username, tries: tries+1
|
71
|
-
end
|
72
|
-
|
73
|
-
def set_ssh_config!(hostname, username:, ip:)
|
74
|
-
FileUtils.touch(SSH_CONFIG_PATH)
|
75
|
-
config = ConfigFile.new
|
76
|
-
args = {
|
77
|
-
"HostName" => ip,
|
78
|
-
"User" => username,
|
79
|
-
"IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
|
80
|
-
}
|
81
|
-
args.each do |key, value|
|
82
|
-
config.set(hostname, key, value)
|
83
|
-
end
|
84
|
-
config.save
|
85
|
-
end
|
86
|
-
|
87
|
-
def reset_mutagen_session(mutagen_config:, hostname:)
|
88
|
-
return if mutagen_config.nil?
|
89
|
-
alpha_dir = mutagen_config[:alpha]
|
90
|
-
beta_dir = mutagen_config[:beta]
|
91
|
-
|
92
|
-
return if alpha_dir.nil? || beta_dir.nil?
|
93
|
-
|
94
|
-
terminate_mutagen_session
|
95
|
-
create_mutagen_session(
|
96
|
-
alpha_dir: alpha_dir,
|
97
|
-
beta_dir: beta_dir,
|
98
|
-
hostname: hostname,
|
99
|
-
)
|
100
|
-
|
101
|
-
if OS.linux?
|
102
|
-
watch_alpha(alpha_dir: alpha_dir)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def terminate_mutagen_session
|
107
|
-
puts "Terminating mutagen session..."
|
108
|
-
terminate_mutagen_command =
|
109
|
-
%Q(mutagen terminate --label-selector=#{LABEL})
|
110
|
-
terminate_mutagen_stdout,
|
111
|
-
terminate_mutagen_stderr,
|
112
|
-
terminate_mutagen_status =
|
113
|
-
Open3.capture3(terminate_mutagen_command)
|
114
|
-
|
115
|
-
if not terminate_mutagen_status.success?
|
116
|
-
# mutagen prints to stdout and stderr
|
117
|
-
msg = "Failed to terminate mutagen sessions: " \
|
118
|
-
"#{terminate_mutagen_stdout} -" \
|
119
|
-
"#{terminate_mutagen_stderr}"
|
120
|
-
fail msg
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def create_mutagen_session(alpha_dir:, beta_dir:, hostname:)
|
125
|
-
puts "Create mutagen session syncing local #{alpha_dir} " \
|
126
|
-
"with #{hostname} #{beta_dir}"
|
127
|
-
|
128
|
-
create_mutagen_command = [
|
129
|
-
"mutagen sync create",
|
130
|
-
alpha_dir,
|
131
|
-
"#{hostname}:#{beta_dir}",
|
132
|
-
"--label=#{LABEL}",
|
133
|
-
]
|
134
|
-
create_mutagen_command << "--watch-mode-alpha=no-watch" if OS.linux?
|
135
|
-
|
136
|
-
create_mutagen_stdout,
|
137
|
-
create_mutagen_stderr,
|
138
|
-
create_mutagen_status =
|
139
|
-
Open3.capture3(create_mutagen_command.join(" "))
|
140
|
-
|
141
|
-
if not create_mutagen_status.success?
|
142
|
-
# mutagen prints to stdout and stderr
|
143
|
-
msg = "Failed to create mutagen sessions: " \
|
144
|
-
"#{create_mutagen_stdout} -" \
|
145
|
-
"#{create_mutagen_stderr}"
|
146
|
-
fail msg
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def watch_alpha(alpha_dir:)
|
151
|
-
watchman = Watchman.new(dir: alpha_dir)
|
152
|
-
watchman.trigger("mutagen sync flush --label-selector=#{LABEL}")
|
153
|
-
end
|
154
|
-
|
155
|
-
def start_box(name, username)
|
156
|
-
start_command = %Q(gcloud compute instances start #{name})
|
157
|
-
start_stdout, start_stderr, start_status = Open3.capture3(start_command)
|
158
|
-
|
159
|
-
desc = describe(name)
|
160
|
-
ip = desc.ip
|
161
|
-
|
162
|
-
set_ssh_config!(hostname_for(name), {
|
163
|
-
username: username,
|
164
|
-
ip: ip,
|
165
|
-
})
|
166
|
-
end
|
167
|
-
|
168
|
-
def describe(name)
|
169
|
-
puts "Fetching box's description..."
|
170
|
-
|
171
|
-
describe_command = %Q(gcloud compute instances describe #{name})
|
172
|
-
describe_stdout, describe_stderr, describe_status =
|
173
|
-
Open3.capture3(describe_command)
|
174
|
-
|
175
|
-
if !describe_status.success?
|
176
|
-
msg = "Problem fetching the description of #{name}. "
|
177
|
-
msg += "Please ensure you can call `#{describe_command}`.\n"
|
178
|
-
msg += "Error:\n"
|
179
|
-
msg += describe_stderr
|
180
|
-
fail msg
|
181
|
-
end
|
182
|
-
|
183
|
-
Description.new(describe_stdout)
|
184
|
-
end
|
185
|
-
|
186
|
-
def hostname_for(name)
|
187
|
-
[name, "devbox"].join("-")
|
188
|
-
end
|
14
|
+
Box.new(account, options).start
|
189
15
|
end
|
190
16
|
|
191
17
|
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module DevboxLauncher
|
2
|
+
class Box
|
3
|
+
|
4
|
+
WAIT_BOOT_RESCUED_EXCEPTIONS = [
|
5
|
+
Net::SSH::ConnectionTimeout,
|
6
|
+
Net::SSH::Disconnect,
|
7
|
+
Errno::ECONNRESET,
|
8
|
+
Errno::ETIMEDOUT,
|
9
|
+
]
|
10
|
+
WAIT_BOOT_IN_SECONDS = 10.freeze
|
11
|
+
MAX_BOOT_RETRIES = 10
|
12
|
+
DEFAULT_IDENTIFY_FILE_PATH = "~/.ssh/google_compute_engine".freeze
|
13
|
+
SSH_CONFIG_PATH = File.expand_path("~/.ssh/config").freeze
|
14
|
+
CONFIG_PATH = File.expand_path("~/.devbox_launcher.yml").freeze
|
15
|
+
CONFIG = YAML.load_file(CONFIG_PATH).freeze
|
16
|
+
|
17
|
+
attr_reader :account, :options
|
18
|
+
|
19
|
+
def initialize(account, options)
|
20
|
+
@account = account
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
start_stdout, start_stderr, start_status =
|
26
|
+
Open3.capture3(start_cmd)
|
27
|
+
|
28
|
+
set_ssh_config!
|
29
|
+
|
30
|
+
wait_boot
|
31
|
+
|
32
|
+
reset_mutagen_session
|
33
|
+
|
34
|
+
connect_mosh
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_cmd
|
38
|
+
args = {
|
39
|
+
project: config[:project],
|
40
|
+
account: account,
|
41
|
+
zone: config[:zone],
|
42
|
+
}.each_with_object([]) do |(key, val), arr|
|
43
|
+
next if val.blank?
|
44
|
+
arr << ["--#{key}", val].join("=")
|
45
|
+
end.join(" ")
|
46
|
+
|
47
|
+
[
|
48
|
+
"gcloud",
|
49
|
+
"compute",
|
50
|
+
"instances",
|
51
|
+
"start",
|
52
|
+
name,
|
53
|
+
args
|
54
|
+
].join(" ")
|
55
|
+
end
|
56
|
+
|
57
|
+
def connect_mosh
|
58
|
+
return if options[:mosh].nil?
|
59
|
+
|
60
|
+
mosh_cmd = %Q(mosh #{hostname})
|
61
|
+
system(mosh_cmd)
|
62
|
+
end
|
63
|
+
|
64
|
+
def wait_boot(tries: 1)
|
65
|
+
Net::SSH.start(hostname, username, timeout: WAIT_BOOT_IN_SECONDS) do |ssh|
|
66
|
+
puts "[#{ssh.exec!('date').chomp}] Machine booted"
|
67
|
+
end
|
68
|
+
rescue *WAIT_BOOT_RESCUED_EXCEPTIONS
|
69
|
+
puts "Not booted. Waiting #{WAIT_BOOT_IN_SECONDS} seconds before trying again..."
|
70
|
+
|
71
|
+
sleep WAIT_BOOT_IN_SECONDS
|
72
|
+
|
73
|
+
if !description(reload: true).running?
|
74
|
+
puts "Detected that the machine is not running " \
|
75
|
+
"(status is #{description.status}). Booting it..."
|
76
|
+
start
|
77
|
+
end
|
78
|
+
|
79
|
+
fail if tries >= MAX_BOOT_RETRIES
|
80
|
+
|
81
|
+
wait_boot tries: tries+1
|
82
|
+
end
|
83
|
+
|
84
|
+
def description(reload: false)
|
85
|
+
return @description if !reload && @description
|
86
|
+
|
87
|
+
puts "Fetching box's description..."
|
88
|
+
|
89
|
+
describe_stdout, describe_stderr, describe_status =
|
90
|
+
Open3.capture3(describe_cmd)
|
91
|
+
|
92
|
+
if !describe_status.success?
|
93
|
+
msg = "Problem fetching the description of #{name}. "
|
94
|
+
msg += "Please ensure you can call `#{describe_cmd}`.\n"
|
95
|
+
msg += "Error:\n"
|
96
|
+
msg += describe_stderr
|
97
|
+
fail msg
|
98
|
+
end
|
99
|
+
|
100
|
+
@description = Description.new(describe_stdout)
|
101
|
+
end
|
102
|
+
|
103
|
+
def describe_cmd
|
104
|
+
args = {
|
105
|
+
project: config[:project],
|
106
|
+
account: account,
|
107
|
+
}.map do |(key, val)|
|
108
|
+
["--#{key}", val].join("=")
|
109
|
+
end.join(" ")
|
110
|
+
|
111
|
+
[
|
112
|
+
"gcloud",
|
113
|
+
"compute",
|
114
|
+
"instances",
|
115
|
+
"describe",
|
116
|
+
name,
|
117
|
+
args
|
118
|
+
].join(" ")
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_ssh_config!
|
122
|
+
FileUtils.touch(SSH_CONFIG_PATH)
|
123
|
+
config = ConfigFile.new
|
124
|
+
args = {
|
125
|
+
"HostName" => description.ip,
|
126
|
+
"User" => username,
|
127
|
+
"IdentityFile" => DEFAULT_IDENTIFY_FILE_PATH,
|
128
|
+
}
|
129
|
+
args.each do |key, value|
|
130
|
+
config.set(hostname, key, value)
|
131
|
+
end
|
132
|
+
config.save
|
133
|
+
end
|
134
|
+
|
135
|
+
def reset_mutagen_session
|
136
|
+
return if !mutagen_config.configured?
|
137
|
+
|
138
|
+
terminate_mutagen_session
|
139
|
+
create_mutagen_session
|
140
|
+
watch_alpha if OS.linux?
|
141
|
+
end
|
142
|
+
|
143
|
+
def terminate_mutagen_session
|
144
|
+
puts "Terminating mutagen session..."
|
145
|
+
terminate_mutagen_cmd =
|
146
|
+
%Q(mutagen terminate --label-selector=#{label})
|
147
|
+
terminate_mutagen_stdout,
|
148
|
+
terminate_mutagen_stderr,
|
149
|
+
terminate_mutagen_status =
|
150
|
+
Open3.capture3(terminate_mutagen_cmd)
|
151
|
+
|
152
|
+
if not terminate_mutagen_status.success?
|
153
|
+
# mutagen prints to stdout and stderr
|
154
|
+
msg = "Failed to terminate mutagen sessions: " \
|
155
|
+
"#{terminate_mutagen_stdout} -" \
|
156
|
+
"#{terminate_mutagen_stderr}"
|
157
|
+
fail msg
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def label
|
162
|
+
"#{username}=#{name}"
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_mutagen_session
|
166
|
+
puts "Create mutagen session syncing local " \
|
167
|
+
"#{mutagen_config.alpha_dir} with " \
|
168
|
+
"#{hostname} #{mutagen_config.beta_dir}"
|
169
|
+
|
170
|
+
create_mutagen_cmd = [
|
171
|
+
"mutagen sync create",
|
172
|
+
mutagen_config.alpha_dir,
|
173
|
+
"#{hostname}:#{mutagen_config.beta_dir}",
|
174
|
+
"--label=#{label}",
|
175
|
+
]
|
176
|
+
create_mutagen_cmd << "--watch-mode-alpha=no-watch" if OS.linux?
|
177
|
+
|
178
|
+
create_mutagen_stdout,
|
179
|
+
create_mutagen_stderr,
|
180
|
+
create_mutagen_status =
|
181
|
+
Open3.capture3(create_mutagen_cmd.join(" "))
|
182
|
+
|
183
|
+
if not create_mutagen_status.success?
|
184
|
+
# mutagen prints to stdout and stderr
|
185
|
+
msg = "Failed to create mutagen sessions: " \
|
186
|
+
"#{create_mutagen_stdout} -" \
|
187
|
+
"#{create_mutagen_stderr}"
|
188
|
+
fail msg
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def watch_alpha
|
193
|
+
watchman = Watchman.new(dir: mutagen_config.alpha_dir)
|
194
|
+
watchman.trigger("mutagen sync flush --label-selector=#{label}")
|
195
|
+
end
|
196
|
+
|
197
|
+
def name
|
198
|
+
@name ||= config[:box]
|
199
|
+
end
|
200
|
+
|
201
|
+
def hostname
|
202
|
+
[name, username, "devbox"].join("-")
|
203
|
+
end
|
204
|
+
|
205
|
+
def username
|
206
|
+
@username ||= account.gsub(/\W/, "_")
|
207
|
+
end
|
208
|
+
|
209
|
+
def config
|
210
|
+
return @config if @config
|
211
|
+
|
212
|
+
if not CONFIG.has_key?(account)
|
213
|
+
fail "No config in #{CONFIG_PATH} found for #{account}"
|
214
|
+
end
|
215
|
+
|
216
|
+
@config = CONFIG[account].with_indifferent_access
|
217
|
+
end
|
218
|
+
|
219
|
+
def mutagen_config
|
220
|
+
@mutagen_config ||= Mutagen.new(config[:mutagen])
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DevboxLauncher
|
2
|
+
class Mutagen
|
3
|
+
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def configured?
|
11
|
+
[config, alpha_dir, beta_dir].none?(&:nil?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def alpha_dir
|
15
|
+
config[:alpha]
|
16
|
+
end
|
17
|
+
|
18
|
+
def beta_dir
|
19
|
+
config[:beta]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devbox_launcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ramon Tayag
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,7 +164,7 @@ dependencies:
|
|
164
164
|
- - '='
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: 0.0.2
|
167
|
-
description:
|
167
|
+
description:
|
168
168
|
email:
|
169
169
|
- ramon.tayag@gmail.com
|
170
170
|
executables:
|
@@ -189,8 +189,11 @@ files:
|
|
189
189
|
- bin/setup
|
190
190
|
- devbox_launcher.gemspec
|
191
191
|
- lib/devbox_launcher.rb
|
192
|
+
- lib/devbox_launcher/box.rb
|
192
193
|
- lib/devbox_launcher/cli.rb
|
194
|
+
- lib/devbox_launcher/models/box.rb
|
193
195
|
- lib/devbox_launcher/models/description.rb
|
196
|
+
- lib/devbox_launcher/models/mutagen.rb
|
194
197
|
- lib/devbox_launcher/version.rb
|
195
198
|
- lib/devbox_launcher/watchman.rb
|
196
199
|
homepage: https://github.com/bloom-solutions/devbox_launcher
|
@@ -201,7 +204,7 @@ metadata:
|
|
201
204
|
homepage_uri: https://github.com/bloom-solutions/devbox_launcher
|
202
205
|
source_code_uri: https://github.com/bloom-solutions/devbox_launcher
|
203
206
|
changelog_uri: https://github.com/bloom-solutions/devbox_launcher/blob/master/CHANGELOG.md
|
204
|
-
post_install_message:
|
207
|
+
post_install_message:
|
205
208
|
rdoc_options: []
|
206
209
|
require_paths:
|
207
210
|
- lib
|
@@ -216,8 +219,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
216
219
|
- !ruby/object:Gem::Version
|
217
220
|
version: '0'
|
218
221
|
requirements: []
|
219
|
-
rubygems_version: 3.
|
220
|
-
signing_key:
|
222
|
+
rubygems_version: 3.1.4
|
223
|
+
signing_key:
|
221
224
|
specification_version: 4
|
222
225
|
summary: Conveniently launch your devbox
|
223
226
|
test_files: []
|