mrsk 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e1765484e15fa40e1f8af291f09fc56e29a00a2b6e4aba3f30dda44d3982074
4
- data.tar.gz: 302d1eb25ac51ce8f8e2fcfe187863b7ba5caa8b3b55ba7b806d20b7bf7cab7a
3
+ metadata.gz: d8f3425abee6c013a917d960080160e02009976a7980bcf094bad847eb62631a
4
+ data.tar.gz: 153d6c0c396545ce451ac41470de8c325438331c05c0927c93f41d634f039c0d
5
5
  SHA512:
6
- metadata.gz: 9760a98b211d8809102fb8bdc006690ad4242342aae5510c3f7cb632e86f572cc78f9b09203bc25fa4035b3862872d1f3b4063b8ea8c8efaa03a7ef9497481c0
7
- data.tar.gz: ef77a4d3ddefb4e08542e623f9a3be18e86b4d532456a53416eace6dc815b2149acb4797814f0d8210948b096150881428f676af667117e465d53ac438da8f47
6
+ metadata.gz: e4de84f5283ee1dde444b9a2107e9d67ae7229d1c8ddc1b89679767ea8a5ba1568ace8f4f144712ceefcff15d7c85acbcd43079225b741d4b551c40aed55a7a4
7
+ data.tar.gz: b8bfca36906d4b6cfe3f1b332ed9fae1bd6077a10d4388a449a12bb5f308267681700b31d8d89a08a99fa784054f03167d4f776816b6aaff29ec5216bb7e5287
data/README.md CHANGED
@@ -4,9 +4,23 @@ MRSK deploys web apps anywhere from bare metal to cloud VMs using Docker with ze
4
4
 
5
5
  Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I
6
6
 
7
+ Join us on Discord: https://discord.gg/YgHVT7GCXS
8
+
7
9
  ## Installation
8
10
 
9
- Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this:
11
+ If you have a Ruby environment available, you can install MRSK globally with:
12
+
13
+ ```sh
14
+ gem install mrsk
15
+ ```
16
+
17
+ ...otherwise, you can run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use):
18
+
19
+ ```sh
20
+ alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk'
21
+ ```
22
+
23
+ Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this:
10
24
 
11
25
  ```yaml
12
26
  service: hey
@@ -23,7 +37,7 @@ env:
23
37
  - RAILS_MASTER_KEY
24
38
  ```
25
39
 
26
- Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
40
+ Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
27
41
 
28
42
  Now you're ready to deploy to the servers:
29
43
 
@@ -34,7 +48,7 @@ mrsk deploy
34
48
  This will:
35
49
 
36
50
  1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
37
- 2. Install Docker on any server that might be missing it (using apt-get)
51
+ 2. Install Docker on any server that might be missing it (using apt-get): root access is needed via ssh for this.
38
52
  3. Log into the registry both locally and remotely
39
53
  4. Build the image using the standard Dockerfile in the root of the application.
40
54
  5. Push the image to the registry.
@@ -67,6 +81,16 @@ Docker Swarm is much simpler than Kubernetes, but it's still built on the same d
67
81
 
68
82
  Ultimately, there are a myriad of ways to deploy web apps, but this is the toolkit we're using at [37signals](https://37signals.com) to bring [HEY](https://www.hey.com) [home from the cloud](https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0) without losing the advantages of modern containerization tooling.
69
83
 
84
+ ## Running MRSK from Docker
85
+
86
+ MRSK is packaged up in a Docker container similarly to [rails/docked](https://github.com/rails/docked). This will allow you to run MRSK (from your application directory) without having to install any dependencies other than Docker. Add the following alias to your profile configuration to make working with the container more convenient:
87
+
88
+ ```bash
89
+ alias mrsk="docker run -it --rm -v '${PWD}:/workdir' -v '${SSH_AUTH_SOCK}:/ssh-agent' -v /var/run/docker.sock:/var/run/docker.sock -e 'SSH_AUTH_SOCK=/ssh-agent' ghcr.io/mrsked/mrsk:latest"
90
+ ```
91
+
92
+ Since MRSK uses SSH to establish a remote connection, it will need access to your SSH agent. The above command uses a volume mount to make it available inside the container and configures the SSH agent inside the container to make use of it.
93
+
70
94
  ## Configuration
71
95
 
72
96
  ### Using .env file to load required environment variables
@@ -99,9 +123,9 @@ If you need separate env variables for different destinations, you can set them
99
123
 
100
124
  #### Bitwarden as a secret store
101
125
 
102
- If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
126
+ If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
103
127
 
104
- You can store `SOME_SECRET` in a secure note in bitwarden vault.
128
+ You can store `SOME_SECRET` in a secure note in bitwarden vault.
105
129
 
106
130
  ```
107
131
  $ bw list items --search SOME_SECRET | jq
@@ -140,7 +164,7 @@ SOME_SECRET=<%= `bw get notes 123123123-1232-4224-222f-234234234234 --session #{
140
164
  <% else raise ArgumentError, "session_token token missing" end %>
141
165
  ```
142
166
 
143
- Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env`
167
+ Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env`
144
168
 
145
169
 
146
170
  ### Using another registry than Docker Hub
@@ -150,9 +174,9 @@ The default registry is Docker Hub, but you can change it using `registry/server
150
174
  ```yaml
151
175
  registry:
152
176
  server: registry.digitalocean.com
153
- username:
177
+ username:
154
178
  - DOCKER_REGISTRY_TOKEN
155
- password:
179
+ password:
156
180
  - DOCKER_REGISTRY_TOKEN
157
181
  ```
158
182
 
@@ -222,6 +246,12 @@ volumes:
222
246
  - "/local/path:/container/path"
223
247
  ```
224
248
 
249
+ ### MRSK env variables
250
+
251
+ The following env variables are set when your container runs:
252
+
253
+ `MRSK_CONTAINER_NAME` : this contains the current container name and version
254
+
225
255
  ### Using different roles for servers
226
256
 
227
257
  If your application uses separate hosts for running jobs or other roles beyond the default web running, you can specify these hosts in a dedicated role with a new entrypoint command like so:
@@ -256,12 +286,12 @@ servers:
256
286
 
257
287
  You can specialize the default Traefik rules by setting labels on the containers that are being started:
258
288
 
259
- ```
289
+ ```yaml
260
290
  labels:
261
- traefik.http.routers.hey.rule: Host(\`app.hey.com\`)
291
+ traefik.http.routers.hey.rule: Host(`app.hey.com`)
262
292
  ```
263
293
 
264
- Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
294
+ Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
265
295
 
266
296
  This allows you to run multiple applications on the same server sharing the same Traefik instance and port.
267
297
  See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules.
@@ -303,6 +333,29 @@ servers:
303
333
 
304
334
  That'll start the job containers with `docker run ... --cap-add --cpu-count 4 ...`.
305
335
 
336
+ ### Configuring logging
337
+
338
+ You can configure the logging driver and options passed to Docker using `logging`:
339
+
340
+ ```yaml
341
+ logging:
342
+ driver: awslogs
343
+ options:
344
+ awslogs-region: "eu-central-2"
345
+ awslogs-group: "my-app"
346
+ ```
347
+
348
+ If nothing is configured, the default option `max-size=10m` is used for all containers. The default logging driver of Docker is `json-file`.
349
+
350
+ ### Using a different stop wait time
351
+
352
+ On a new deploy, each old running container is gracefully shut down with a `SIGTERM`, and after a grace period of `10` seconds a `SIGKILL` is sent.
353
+ You can configure this value via the `stop_wait_time` option:
354
+
355
+ ```yaml
356
+ stop_wait_time: 30
357
+ ```
358
+
306
359
  ### Using remote builder for native multi-arch
307
360
 
308
361
  If you're developing on ARM64 (like Apple Silicon), but you want to deploy on AMD64 (x86 64-bit), you can use multi-architecture images. By default, MRSK will setup a local buildx configuration that does this through QEMU emulation. But this can be quite slow, especially on the first build.
@@ -408,6 +461,46 @@ traefik:
408
461
  host_port: 8080
409
462
  ```
410
463
 
464
+ ### Configure docker options for traefik
465
+
466
+ We allow users to pass additional docker options to the trafik container like
467
+
468
+ ```yaml
469
+ traefik:
470
+ options:
471
+ publish:
472
+ - 8080:8080
473
+ volumes:
474
+ - /tmp/example.json:/tmp/example.json
475
+ memory: 512m
476
+ ```
477
+
478
+ This will start the traefik container with a command like: `docker run ... --volume /tmp/example.json:/tmp/example.json --publish 8080:8080 `
479
+
480
+
481
+ ### Configure alternate entrypoints for traefik
482
+
483
+ You can configure multiple entrypoints for traefik like so:
484
+
485
+ ```yaml
486
+ service: myservice
487
+
488
+ labels:
489
+ traefik.tcp.routers.other.rule: 'HostSNI(`*`)'
490
+ traefik.tcp.routers.other.entrypoints: otherentrypoint
491
+ traefik.tcp.services.other.loadbalancer.server.port: 9000
492
+ traefik.http.routers.myservice.entrypoints: web
493
+ traefik.http.services.myservice.loadbalancer.server.port: 8080
494
+
495
+ traefik:
496
+ options:
497
+ publish:
498
+ - 9000:9000
499
+ args:
500
+ entrypoints.web.address: ':80'
501
+ entrypoints.otherentrypoint.address: ':9000'
502
+ ```
503
+
411
504
  ### Configuring build args for new images
412
505
 
413
506
  Build arguments that aren't secret can also be configured:
@@ -427,7 +520,7 @@ FROM ruby:$RUBY_VERSION-slim as base
427
520
 
428
521
  ### Using accessories for database, cache, search services
429
522
 
430
- You can manage your accessory services via MRSK as well. The services will build off public images, and will not be automatically updated when you deploy:
523
+ You can manage your accessory services via MRSK as well. Accessories are long-lived services that your app depends on. They are not updated when you deploy.
431
524
 
432
525
  ```yaml
433
526
  accessories:
@@ -442,16 +535,44 @@ accessories:
442
535
  - MYSQL_ROOT_PASSWORD
443
536
  volumes:
444
537
  - /var/lib/mysql:/var/lib/mysql
538
+ options:
539
+ cpus: 4
540
+ memory: "2GB"
445
541
  redis:
446
542
  image: redis:latest
447
- host: 1.1.1.4
543
+ role:
544
+ - web
448
545
  port: "36379:6379"
449
546
  volumes:
450
547
  - /var/lib/redis:/data
548
+ internal-example:
549
+ image: registry.digitalocean.com/user/otherservice:latest
550
+ host: 1.1.1.5
551
+ port: 44444
552
+ ```
553
+
554
+ The hosts that the accessories will run on can be specified by hosts or roles:
555
+
556
+ ```yaml
557
+ # Single host
558
+ mysql:
559
+ host: 1.1.1.1
560
+ # Multiple hosts
561
+ redis:
562
+ hosts:
563
+ - 1.1.1.1
564
+ - 1.1.1.2
565
+ # By role
566
+ monitoring:
567
+ roles:
568
+ - web
569
+ - jobs
451
570
  ```
452
571
 
453
572
  Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `mrsk accessory` for all the commands possible.
454
573
 
574
+ Accessory images must be public or tagged in your private registry.
575
+
455
576
  ### Using Cron
456
577
 
457
578
  You can use a specific container to run your Cron jobs:
@@ -616,6 +737,30 @@ Note that by default old containers are pruned after 3 days when you run `mrsk d
616
737
 
617
738
  If you wish to remove the entire application, including Traefik, containers, images, and registry session, you can run `mrsk remove`. This will leave the servers clean.
618
739
 
740
+ ## Locking
741
+
742
+ Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock` directory on the primary server.
743
+
744
+ You can check the lock status with:
745
+
746
+ ```
747
+ mrsk lock status
748
+
749
+ Locked by: AN Other at 2023-03-24 09:49:03 UTC
750
+ Version: 77f45c0686811c68989d6576748475a60bf53fc2
751
+ Message: Automatic deploy lock
752
+ ```
753
+
754
+ You can also manually acquire and release the lock
755
+
756
+ ```
757
+ mrsk lock acquire -m "Doing maintanence"
758
+ ```
759
+
760
+ ```
761
+ mrsk lock release
762
+ ```
763
+
619
764
  ## Stage of development
620
765
 
621
766
  This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
@@ -1,33 +1,38 @@
1
1
  class Mrsk::Cli::Accessory < Mrsk::Cli::Base
2
2
  desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
3
3
  def boot(name)
4
- if name == "all"
5
- MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
6
- else
7
- with_accessory(name) do |accessory|
8
- directories(name)
9
- upload(name)
4
+ with_lock do
5
+ if name == "all"
6
+ MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
7
+ else
8
+ with_accessory(name) do |accessory|
9
+ directories(name)
10
+ upload(name)
10
11
 
11
- on(accessory.host) do
12
- execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
13
- execute *accessory.run
14
- end
12
+ on(accessory.hosts) do
13
+ execute *MRSK.registry.login
14
+ execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
15
+ execute *accessory.run
16
+ end
15
17
 
16
- audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
18
+ audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
19
+ end
17
20
  end
18
21
  end
19
22
  end
20
23
 
21
24
  desc "upload [NAME]", "Upload accessory files to host", hide: true
22
25
  def upload(name)
23
- with_accessory(name) do |accessory|
24
- on(accessory.host) do
25
- accessory.files.each do |(local, remote)|
26
- accessory.ensure_local_file_present(local)
27
-
28
- execute *accessory.make_directory_for(remote)
29
- upload! local, remote
30
- execute :chmod, "755", remote
26
+ with_lock do
27
+ with_accessory(name) do |accessory|
28
+ on(accessory.hosts) do
29
+ accessory.files.each do |(local, remote)|
30
+ accessory.ensure_local_file_present(local)
31
+
32
+ execute *accessory.make_directory_for(remote)
33
+ upload! local, remote
34
+ execute :chmod, "755", remote
35
+ end
31
36
  end
32
37
  end
33
38
  end
@@ -35,10 +40,12 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
35
40
 
36
41
  desc "directories [NAME]", "Create accessory directories on host", hide: true
37
42
  def directories(name)
38
- with_accessory(name) do |accessory|
39
- on(accessory.host) do
40
- accessory.directories.keys.each do |host_path|
41
- execute *accessory.make_directory(host_path)
43
+ with_lock do
44
+ with_accessory(name) do |accessory|
45
+ on(accessory.hosts) do
46
+ accessory.directories.keys.each do |host_path|
47
+ execute *accessory.make_directory(host_path)
48
+ end
42
49
  end
43
50
  end
44
51
  end
@@ -46,38 +53,46 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
46
53
 
47
54
  desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
48
55
  def reboot(name)
49
- with_accessory(name) do |accessory|
50
- stop(name)
51
- remove_container(name)
52
- boot(name)
56
+ with_lock do
57
+ with_accessory(name) do |accessory|
58
+ stop(name)
59
+ remove_container(name)
60
+ boot(name)
61
+ end
53
62
  end
54
63
  end
55
64
 
56
65
  desc "start [NAME]", "Start existing accessory container on host"
57
66
  def start(name)
58
- with_accessory(name) do |accessory|
59
- on(accessory.host) do
60
- execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
61
- execute *accessory.start
67
+ with_lock do
68
+ with_accessory(name) do |accessory|
69
+ on(accessory.hosts) do
70
+ execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
71
+ execute *accessory.start
72
+ end
62
73
  end
63
74
  end
64
75
  end
65
76
 
66
77
  desc "stop [NAME]", "Stop existing accessory container on host"
67
78
  def stop(name)
68
- with_accessory(name) do |accessory|
69
- on(accessory.host) do
70
- execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
71
- execute *accessory.stop, raise_on_non_zero_exit: false
79
+ with_lock do
80
+ with_accessory(name) do |accessory|
81
+ on(accessory.hosts) do
82
+ execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
83
+ execute *accessory.stop, raise_on_non_zero_exit: false
84
+ end
72
85
  end
73
86
  end
74
87
  end
75
88
 
76
89
  desc "restart [NAME]", "Restart existing accessory container on host"
77
90
  def restart(name)
78
- with_accessory(name) do
79
- stop(name)
80
- start(name)
91
+ with_lock do
92
+ with_accessory(name) do
93
+ stop(name)
94
+ start(name)
95
+ end
81
96
  end
82
97
  end
83
98
 
@@ -87,7 +102,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
87
102
  MRSK.accessory_names.each { |accessory_name| details(accessory_name) }
88
103
  else
89
104
  with_accessory(name) do |accessory|
90
- on(accessory.host) { puts capture_with_info(*accessory.info) }
105
+ on(accessory.hosts) { puts capture_with_info(*accessory.info) }
91
106
  end
92
107
  end
93
108
  end
@@ -108,14 +123,14 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
108
123
 
109
124
  when options[:reuse]
110
125
  say "Launching command from existing container...", :magenta
111
- on(accessory.host) do
126
+ on(accessory.hosts) do
112
127
  execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
113
128
  capture_with_info(*accessory.execute_in_existing_container(cmd))
114
129
  end
115
130
 
116
131
  else
117
132
  say "Launching command from new container...", :magenta
118
- on(accessory.host) do
133
+ on(accessory.hosts) do
119
134
  execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
120
135
  capture_with_info(*accessory.execute_in_new_container(cmd))
121
136
  end
@@ -134,7 +149,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
134
149
 
135
150
  if options[:follow]
136
151
  run_locally do
137
- info "Following logs on #{accessory.host}..."
152
+ info "Following logs on #{accessory.hosts}..."
138
153
  info accessory.follow_logs(grep: grep)
139
154
  exec accessory.follow_logs(grep: grep)
140
155
  end
@@ -142,25 +157,27 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
142
157
  since = options[:since]
143
158
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
144
159
 
145
- on(accessory.host) do
160
+ on(accessory.hosts) do
146
161
  puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
147
162
  end
148
163
  end
149
164
  end
150
165
  end
151
166
 
152
- desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to remove all accessories)"
167
+ desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
153
168
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
154
169
  def remove(name)
155
- if name == "all"
156
- MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
157
- else
158
- if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
159
- with_accessory(name) do
160
- stop(name)
161
- remove_container(name)
162
- remove_image(name)
163
- remove_service_directory(name)
170
+ with_lock do
171
+ if name == "all"
172
+ MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
173
+ else
174
+ if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
175
+ with_accessory(name) do
176
+ stop(name)
177
+ remove_container(name)
178
+ remove_image(name)
179
+ remove_service_directory(name)
180
+ end
164
181
  end
165
182
  end
166
183
  end
@@ -168,29 +185,35 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
168
185
 
169
186
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
170
187
  def remove_container(name)
171
- with_accessory(name) do |accessory|
172
- on(accessory.host) do
173
- execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
174
- execute *accessory.remove_container
188
+ with_lock do
189
+ with_accessory(name) do |accessory|
190
+ on(accessory.hosts) do
191
+ execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
192
+ execute *accessory.remove_container
193
+ end
175
194
  end
176
195
  end
177
196
  end
178
197
 
179
198
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
180
199
  def remove_image(name)
181
- with_accessory(name) do |accessory|
182
- on(accessory.host) do
183
- execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
184
- execute *accessory.remove_image
200
+ with_lock do
201
+ with_accessory(name) do |accessory|
202
+ on(accessory.hosts) do
203
+ execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
204
+ execute *accessory.remove_image
205
+ end
185
206
  end
186
207
  end
187
208
  end
188
209
 
189
210
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
190
211
  def remove_service_directory(name)
191
- with_accessory(name) do |accessory|
192
- on(accessory.host) do
193
- execute *accessory.remove_service_directory
212
+ with_lock do
213
+ with_accessory(name) do |accessory|
214
+ on(accessory.hosts) do
215
+ execute *accessory.remove_service_directory
216
+ end
194
217
  end
195
218
  end
196
219
  end