standard-procedure-anvil 0.1.3.1 → 0.1.4

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: 1768df6c04673154198ef33d8a3af8e58b330547e98816c419829f182473f1c9
4
- data.tar.gz: 9a628290e842fae2e0b50c5527483904277fc9e58a1b00ca0ec4bc3f4a8e7b7b
3
+ metadata.gz: 50580aabd8ea329997f9a044178f08790aa12719e6d412da4a6a7a3b6c335fff
4
+ data.tar.gz: c4ec1dbbd6ccc1945e6fab5f76f988578bd0df0cef15cc7801b2bbdfda5f6a30
5
5
  SHA512:
6
- metadata.gz: 1a166f49a00d2ae2bbd4cffa8dedc0b85bf55f50e8f5e8eb9b70a1484e3ef01ad2792d4aeafd2a04c1c84aa11f091516a59ff7ed1fde5a97e90433e204c9666e
7
- data.tar.gz: b7f949bee69bc7f93497c170f20a07533cb4a22af1b19a9cc9b979c5536642294dc6ff0f18b362f03af3eebea50e1e1b13a665edcac88b98ab8a98e0c6ed4439
6
+ metadata.gz: e39c66044138540191d4214f5fa0b7f46d76f037b5268315f7fdb19cca214e545f93e9d89ce69d939339ca0133b7fa209281a21938ddd976d17e79160bc00167
7
+ data.tar.gz: 968a6df094c5f7846c23af2148b04c3671ce15c8abffce3ba407c0e3014aee4bb15e64873a6cfa0287dcd19b6193883efa259b4f8c9eb2b047abb12f6fa5ed95
@@ -0,0 +1,126 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin, docker
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - ufw
12
+ - wget
13
+ - apt-transport-https
14
+ - docker.io
15
+ - docker-compose
16
+ - mysql-client
17
+ - libmysqlclient-dev
18
+ package_update: true
19
+ package_upgrade: true
20
+ runcmd:
21
+ # General server setup
22
+ - timedatectl set-timezone UTC
23
+ # Prepare for OpenSearch
24
+ - swapoff -a
25
+ - echo "vm.max_map_count=262144" > /etc/sysctl.d/98-opensearch.conf
26
+ - sysctl -p /etc/sysctl.d/98-opensearch.conf
27
+ # Install MySQL
28
+ - echo "mysql-server mysql-server/root_password password root" | sudo debconf-set-selections
29
+ - echo "mysql-server mysql-server/root_password_again password root" | sudo debconf-set-selections
30
+ - sudo apt-get -y install mysql-server
31
+ - |
32
+ cat >> /etc/mysql/mysql.conf.d/utf8.cnf << CONF
33
+ [client]
34
+ default-character-set=utf8mb4
35
+
36
+ [mysql]
37
+ default-character-set=utf8mb4
38
+
39
+ [mysqld]
40
+ init_connect='SET collation_connection = utf8mb4_unicode_ci'
41
+ init_connect='SET NAMES utf8mb4'
42
+ character-set-server=utf8mb4
43
+ collation-server=utf8mb4_unicode_ci
44
+ skip-character-set-client-handshake
45
+ CONF
46
+ - sed -i -e '/^\(#\|\)bind-address/s/^.*$/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
47
+ # Start MySQL
48
+ - systemctl start mysql.service
49
+ # Fail2Ban setup
50
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
51
+ - systemctl enable fail2ban
52
+ # UFW and SSH setup
53
+ - ufw allow 22/tcp
54
+ - ufw allow 80/tcp
55
+ - ufw allow 443/tcp
56
+ - ufw enable
57
+ # Harden SSH
58
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
59
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
60
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
61
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
62
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
63
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
64
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
65
+ - sed -i '$a AllowUsers %{USER} dokku' /etc/ssh/sshd_config
66
+ # Dokku setup
67
+ - echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
68
+ - wget https://dokku.com/install/v0.30.7/bootstrap.sh && sudo DOKKU_TAG=v0.30.7 bash bootstrap.sh
69
+ - cat /home/%{USER}/.ssh/authorized_keys | dokku ssh-keys:add admin
70
+ - dokku plugin:install https://github.com/dokku/dokku-cron-restart.git cron-restart
71
+ - dokku plugin:install https://github.com/dokku/dokku-maintenance.git maintenance
72
+ - dokku plugin:install https://github.com/dokku/dokku-redis.git redis
73
+ - dokku plugin:install https://github.com/dokku/dokku-mariadb.git mariadb
74
+ - dokku plugin:install https://github.com/dokku/dokku-memcached.git memcached
75
+ - dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git letsencrypt
76
+ - dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=sysadmin@echodek.co
77
+ - dokku git:set --global deploy-branch main
78
+ # OpenSearch setup
79
+ - mkdir -p /etc/opensearch
80
+ - docker pull opensearchproject/opensearch:latest
81
+ - docker pull opensearchproject/opensearch-dashboards:latest
82
+ - |
83
+ cat >> /etc/opensearch/docker-compose.yml << EOF
84
+ version: '3'
85
+ services:
86
+ search_db:
87
+ image: opensearchproject/opensearch:latest
88
+ container_name: search_db
89
+ environment:
90
+ - discovery.type=single-node
91
+ - node.name=search_db
92
+ - bootstrap.memory_lock=true
93
+ - plugins.security.disabled=true
94
+ - "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m"
95
+ ulimits:
96
+ memlock:
97
+ soft: -1
98
+ hard: -1
99
+ nofile:
100
+ soft: 65536
101
+ hard: 65536
102
+ volumes:
103
+ - opensearch_data:/usr/share/opensearch/data
104
+ ports:
105
+ - 9200:9200
106
+ - 9600:9600
107
+ volumes:
108
+ opensearch_data:
109
+ EOF
110
+ - |
111
+ cat >> /etc/systemd/system/opensearch.service << EOF
112
+ Description=OpenSearch container
113
+ Requires=docker.service
114
+ After=docker.service
115
+ [Service]
116
+ WorkingDirectory=/etc/opensearch
117
+ Restart=always
118
+ ExecStart=/usr/bin/docker-compose up
119
+ ExecStop=/usr/bin/docker-compose down
120
+ [Install]
121
+ WantedBy=multi-user.target
122
+ EOF
123
+ - systemctl daemon-reload
124
+ - systemctl enable opensearch.service
125
+ - service opensearch start
126
+ - reboot
@@ -0,0 +1 @@
1
+ 2b49b3137b8c3bbe046aa080a6bd695eedc5afe6cd6c01ff4f37a22864f76ba7c3256c5e5a4f961977eac7dac1119cc805988365a7fa505530bd0a4688cba039
data/lib/anvil/app.rb CHANGED
@@ -7,7 +7,7 @@ module Anvil
7
7
  class App < Anvil::SubCommandBase
8
8
  require_relative "app/env"
9
9
 
10
- desc "env", "Generate environment variables for an app"
10
+ desc "env /path/to/config.yml", "Generate environment variables for an app"
11
11
  long_desc <<-DESC
12
12
  List the environment variables for an app (on a given host)
13
13
 
@@ -16,13 +16,6 @@ module Anvil
16
16
 
17
17
  If the /path/to/config is not supplied, it defaults to deploy.yml
18
18
 
19
- Options:
20
-
21
- --host, -h: The server that the environment variables should be generated for - only required if multiple servers are configured
22
-
23
- --secrets, -s: The path to a file containing secrets to be injected into the environment variables
24
-
25
- --secrets-stdin, -S: Read secrets from STDIN instead of a file
26
19
  DESC
27
20
  option :host, type: :string, default: nil, aliases: "-h"
28
21
  option :secrets, type: :string, default: nil, aliases: "-s"
@@ -33,7 +26,7 @@ module Anvil
33
26
  puts Anvil::App::Env.new(configuration, options[:host], secrets).call
34
27
  end
35
28
 
36
- desc "install", "Install an app"
29
+ desc "install /path/to/config.yml", "Install an app"
37
30
  long_desc <<-DESC
38
31
  Install an app on the hosts specified in the configuration.
39
32
 
@@ -45,12 +38,10 @@ module Anvil
45
38
  anvil app install /path/to/config
46
39
  If the /path/to/config is not supplied, it defaults to deploy.yml
47
40
 
48
- Options:
49
-
50
- --secrets, -s: The path to a file containing secrets to be injected into the environment variables
51
-
52
- --secrets-stdin, -S: Read secrets from STDIN instead of a file
41
+ If --secrets-stdin is specified then additional environment variable values will be read from STDIN, if --secrets=/path/to/secrets is specified then they will be read from the file specified. This is so you can specify environment variables that you do not want stored in source control. These should be formatted as "VAR=value VAR2=value2" etc.
53
42
  DESC
43
+ option :secrets, type: :string, default: nil, aliases: "-s"
44
+ option :secrets_stdin, type: :boolean, default: false, aliases: "-S"
54
45
  def install filename = "deploy.yml"
55
46
  configuration = YAML.load_file(filename)
56
47
  secrets = read_secrets filename: options[:secrets], stdin: options[:secrets_stdin]
data/lib/anvil/cli.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "cloudinit"
5
- require_relative "app"
6
4
 
7
5
  module Anvil
6
+ require_relative "cloudinit"
7
+ require_relative "app"
8
+ require_relative "mysql"
8
9
  class Cli < Thor
9
10
  desc "cloudinit", "Generate a cloudinit configuration"
10
11
  subcommand "cloudinit", Anvil::Cloudinit
11
12
 
13
+ desc "mysql", "Manage mysql"
14
+ subcommand "mysql", Anvil::Mysql
15
+
12
16
  desc "app", "Install or deploy a dokku app"
13
17
  subcommand "app", Anvil::App
14
18
 
@@ -13,16 +13,13 @@ module Anvil
13
13
  end
14
14
  end
15
15
 
16
- desc "generate", "Generate a cloudinit configuration"
16
+ desc "generate configuration", "Generate a cloudinit configuration"
17
17
  long_desc <<-DESC
18
18
  Generate a cloudinit configuration for a server
19
19
 
20
20
  Example:
21
21
  anvil cloudinit generate mysql.ubuntu-22 --user dbuser --public_key ~/.ssh/my_key.pub
22
22
 
23
- Options:
24
- --user, -u: The user to create on the server - defaults to app
25
- --public_key, -k: The path to the public key file that will be installed for the user - default to ~/.ssh/id_rsa.pub
26
23
  DESC
27
24
  option :user, type: :string, default: "app", aliases: "-u"
28
25
  option :public_key, type: :string, default: "~/.ssh/id_rsa.pub", aliases: "-k"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../subcommand"
4
+ require "rujitsu"
5
+ module Anvil
6
+ class Mysql
7
+ require_relative "password"
8
+ require_relative "database_creator"
9
+ require_relative "user_creator"
10
+
11
+ class Create < Anvil::SubCommandBase
12
+ include Password
13
+
14
+ desc "database db_name user host", "Create a mysql database"
15
+ long_desc <<-DESC
16
+ Create a mysql database by SSHing into a server and then connecting to MySQL to create the database.
17
+
18
+ Example:
19
+
20
+ anvil mysql create database my_database user server.example.com --mysql-user root --mysql-host data.internal.example.com
21
+
22
+ This command will SSH into user@server.example.com, then connect to the MySQL server on data.internal.example.com as root to create the database. It will take the root password from STDIN.
23
+
24
+ The assumption is that your MySQL server is not accessible from your local development machine.
25
+
26
+ The SSH command assumes you have a current SSH Agent to load your private key.
27
+
28
+ You can optionally supply a password for the MySQL user, or it will be read from STDIN.
29
+ DESC
30
+ option :mysql_user, type: :string, default: "root", aliases: "-m"
31
+ option :mysql_password, type: :string, default: nil, aliases: "-p"
32
+ option :mysql_host, type: :string, default: "localhost", aliases: "-H"
33
+ option :mysql_port, type: :numeric, default: 3306, aliases: "-P"
34
+ def database db_name, user, host
35
+ password = get_password_from options[:mysql_password]
36
+ Anvil::Mysql::DatabaseCreator.new(db_name, user, host, options[:mysql_user], password, options[:mysql_host], options[:mysql_port]).call
37
+ end
38
+
39
+ desc "user db_username user host", "Create a mysql user"
40
+ long_desc <<-DESC
41
+ Create a database user by SSHing into a server and then connecting to MySQL to create the user.
42
+
43
+ You can optionally specify a password for your database user, or it will be generated for you and returned to STDOUT.
44
+
45
+ Example:
46
+
47
+ anvil mysql create user my_user user server.example.com --mysql-user root --mysql-host data.internal.example.com
48
+
49
+ This command will SSH into user@server.example.com, then connect to the MySQL server on data.internal.example.com as root to create the user. It will take the root password from STDIN.
50
+
51
+ The assumption is that your MySQL server is not accessible from your local development machine.
52
+
53
+ The SSH command assumes you have a current SSH Agent to load your private key.
54
+
55
+ You can optionally supply a password for the MySQL user, or it will be read from STDIN.
56
+ DESC
57
+ option :db_password, type: :string, default: nil, aliases: "-d"
58
+ option :mysql_user, type: :string, default: "root", aliases: "-m"
59
+ option :mysql_password, type: :string, default: nil, aliases: "-p"
60
+ option :mysql_host, type: :string, default: "localhost", aliases: "-H"
61
+ option :mysql_port, type: :numeric, default: 3306, aliases: "-P"
62
+ def user db_user, user, host
63
+ mysql_password = options[:mysql_password] || $stdin.gets.chomp
64
+ db_password = options[:db_password] || "#{4.random_letters}-#{4.random_characters}-#{4.random_numbers}-#{4.random_letters}-#{4.random_characters}"
65
+ Anvil::Mysql::UserCreator.new(db_user, db_password, user, host, options[:mysql_user], mysql_password, options[:mysql_host], options[:mysql_port]).call
66
+ puts db_password if options[:db_password].nil?
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "../ssh_executor"
2
+ require_relative "../logger"
3
+ require_relative "../script_runner"
4
+
5
+ module Anvil
6
+ class Mysql
7
+ class DatabaseCreator < Struct.new(:db_name, :user, :host, :mysql_user, :mysql_password, :mysql_host, :mysql_port)
8
+ def call
9
+ ScriptRunner.new(script, user, host, logger).call
10
+ end
11
+
12
+ def db_script
13
+ "CREATE DATABASE #{db_name};"
14
+ end
15
+
16
+ def script
17
+ "mysql -u#{mysql_user} -p#{mysql_password} -h #{mysql_host} -P #{mysql_port} -e \"#{db_script}\""
18
+ end
19
+
20
+ def logger
21
+ Anvil::Logger.new(self.class.name)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../subcommand"
4
+ module Anvil
5
+ class Mysql
6
+ require_relative "password"
7
+ require_relative "privileges_granter"
8
+ class Grant < Anvil::SubCommandBase
9
+ include Password
10
+
11
+ desc "all db_name db_username user host", "Grant all privileges on db_name to db_user"
12
+ option :mysql_user, type: :string, default: "root", aliases: "-m"
13
+ option :mysql_password, type: :string, default: nil, aliases: "-p"
14
+ option :mysql_host, type: :string, default: "localhost", aliases: "-H"
15
+ option :mysql_port, type: :numeric, default: 3306, aliases: "-P"
16
+ def all db_name, db_username, user, host
17
+ password = get_password_from options[:mysql_password]
18
+ Anvil::Mysql::PrivilegesGranter.new(db_name, db_username, user, host, options[:mysql_user], password, options[:mysql_host], options[:mysql_port]).call
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module Anvil
2
+ class Mysql
3
+ module Password
4
+ protected
5
+
6
+ def get_password_from option
7
+ if option.nil?
8
+ puts "MySQL password:"
9
+ $stdin.gets.chomp
10
+ else
11
+ option
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../ssh_executor"
2
+ require_relative "../logger"
3
+ require_relative "../script_runner"
4
+ module Anvil
5
+ class Mysql
6
+ class PrivilegesGranter < Struct.new(:db_name, :db_user, :user, :host, :mysql_user, :mysql_password, :mysql_host, :mysql_port)
7
+ def call
8
+ ScriptRunner.new(script, user, host, logger).call
9
+ end
10
+
11
+ def db_script
12
+ "GRANT ALL PRIVILEGES on #{db_name}.* to '#{db_user}'@'%';"
13
+ end
14
+
15
+ def script
16
+ "mysql -u#{mysql_user} -p#{mysql_password} -h #{mysql_host} -P #{mysql_port} -e \"#{db_script}\""
17
+ end
18
+
19
+ def logger
20
+ Anvil::Logger.new(self.class.name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../ssh_executor"
2
+ require_relative "../logger"
3
+ require_relative "../script_runner"
4
+ module Anvil
5
+ class Mysql
6
+ class UserCreator < Struct.new(:db_user, :db_password, :user, :host, :mysql_user, :mysql_password, :mysql_host, :mysql_port)
7
+ def call
8
+ ScriptRunner.new(script, user, host, logger).call
9
+ end
10
+
11
+ def db_script
12
+ "CREATE USER '#{db_user}'@'%' IDENTIFIED BY '#{db_password}';"
13
+ end
14
+
15
+ def script
16
+ "mysql -u#{mysql_user} -p#{mysql_password} -h #{mysql_host} -P #{mysql_port} -e \"#{db_script}\""
17
+ end
18
+
19
+ def logger
20
+ Anvil::Logger.new(self.class.name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subcommand"
4
+
5
+ module Anvil
6
+ class Mysql < Anvil::SubCommandBase
7
+ require_relative "mysql/create"
8
+ require_relative "mysql/grant"
9
+
10
+ desc "create", "Create mysql databases and users "
11
+ subcommand "create", Anvil::Mysql::Create
12
+
13
+ desc "grant", "Grant mysql permissions"
14
+ subcommand "grant", Anvil::Mysql::Grant
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "ssh_executor"
2
+ module Anvil
3
+ class Mysql
4
+ class ScriptRunner < Struct.new(:script, :user, :host, :logger)
5
+ def call
6
+ SshExecutor.new(host, user, logger).call do |ssh|
7
+ ssh.exec! script, "SSH"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -10,7 +10,7 @@ require "net/ssh"
10
10
  module Anvil
11
11
  class SshExecutor < Struct.new(:hostname, :user, :logger)
12
12
  def call &block
13
- @connection = Net::SSH.start hostname, user, use_agent: true
13
+ @connection = Net::SSH.start hostname, user, use_agent: true, verify_host_key: :accept_new
14
14
  block.call self
15
15
  end
16
16
 
data/lib/anvil/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anvil
4
- VERSION = "0.1.3.1"
4
+ VERSION = "0.1.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-anvil
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3.1
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-26 00:00:00.000000000 Z
11
+ date: 2023-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rujitsu
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: standard-procedure-async
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -96,11 +110,13 @@ files:
96
110
  - LICENSE.txt
97
111
  - README.md
98
112
  - Rakefile
113
+ - assets/cloudinit/dokku.mysql.opensearch.ubuntu-22.yml
99
114
  - assets/cloudinit/dokku.ubuntu-22.yml
100
115
  - assets/cloudinit/memcached.ubuntu-22.yml
101
116
  - assets/cloudinit/mysql.ubuntu-22.yml
102
117
  - assets/cloudinit/opensearch.ubuntu-22.yml
103
118
  - assets/cloudinit/redis.ubuntu-22.yml
119
+ - checksums/standard-procedure-anvil-0.1.4.gem.sha512
104
120
  - exe/anvil
105
121
  - lib/anvil.rb
106
122
  - lib/anvil/app.rb
@@ -112,6 +128,14 @@ files:
112
128
  - lib/anvil/cloudinit/generator.rb
113
129
  - lib/anvil/configuration_reader.rb
114
130
  - lib/anvil/logger.rb
131
+ - lib/anvil/mysql.rb
132
+ - lib/anvil/mysql/create.rb
133
+ - lib/anvil/mysql/database_creator.rb
134
+ - lib/anvil/mysql/grant.rb
135
+ - lib/anvil/mysql/password.rb
136
+ - lib/anvil/mysql/privileges_granter.rb
137
+ - lib/anvil/mysql/user_creator.rb
138
+ - lib/anvil/script_runner.rb
115
139
  - lib/anvil/ssh_executor.rb
116
140
  - lib/anvil/subcommand.rb
117
141
  - lib/anvil/version.rb