hyrb 0.0.2

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +61 -0
  6. data/Rakefile +8 -0
  7. data/bin/hyrb +9 -0
  8. data/hyrb.gemspec +35 -0
  9. data/lib/hyrb/cli.rb +27 -0
  10. data/lib/hyrb/command.rb +74 -0
  11. data/lib/hyrb/commands/ansible.rb +13 -0
  12. data/lib/hyrb/commands/creds.rb +20 -0
  13. data/lib/hyrb/commands/defaults.rb +20 -0
  14. data/lib/hyrb/commands/developers.rb +15 -0
  15. data/lib/hyrb/commands/digital_ocean.rb +15 -0
  16. data/lib/hyrb/commands/environment.rb +11 -0
  17. data/lib/hyrb/commands/github.rb +15 -0
  18. data/lib/hyrb/commands/hipchat.rb +20 -0
  19. data/lib/hyrb/commands/project.rb +36 -0
  20. data/lib/hyrb/commands/provision.rb +17 -0
  21. data/lib/hyrb/commands/rackspace.rb +11 -0
  22. data/lib/hyrb/model.rb +96 -0
  23. data/lib/hyrb/models/ansible_host.rb +28 -0
  24. data/lib/hyrb/models/ansible_site.rb +46 -0
  25. data/lib/hyrb/models/cache.rb +35 -0
  26. data/lib/hyrb/models/creds.rb +24 -0
  27. data/lib/hyrb/models/defaults.rb +11 -0
  28. data/lib/hyrb/models/developer.rb +28 -0
  29. data/lib/hyrb/models/environment.rb +41 -0
  30. data/lib/hyrb/models/project.rb +35 -0
  31. data/lib/hyrb/pipeline.rb +63 -0
  32. data/lib/hyrb/task.rb +46 -0
  33. data/lib/hyrb/tasks/ansible.rb +96 -0
  34. data/lib/hyrb/tasks/creds.rb +39 -0
  35. data/lib/hyrb/tasks/defaults.rb +43 -0
  36. data/lib/hyrb/tasks/developers.rb +120 -0
  37. data/lib/hyrb/tasks/digital_ocean.rb +84 -0
  38. data/lib/hyrb/tasks/environment.rb +56 -0
  39. data/lib/hyrb/tasks/github.rb +120 -0
  40. data/lib/hyrb/tasks/google.rb +15 -0
  41. data/lib/hyrb/tasks/hipchat.rb +76 -0
  42. data/lib/hyrb/tasks/project/bootstrap.rb +88 -0
  43. data/lib/hyrb/tasks/project.rb +48 -0
  44. data/lib/hyrb/tasks/provision.rb +91 -0
  45. data/lib/hyrb/tasks/rackspace.rb +95 -0
  46. data/lib/hyrb/version.rb +3 -0
  47. data/lib/hyrb.rb +18 -0
  48. data/lib/templates/ansible/Vagrantfile.erb +21 -0
  49. data/lib/templates/ansible/roles/db/handlers/main.yml +2 -0
  50. data/lib/templates/ansible/roles/db/tasks/main.yml +24 -0
  51. data/lib/templates/ansible/roles/lamp/tasks/main.yml +53 -0
  52. data/lib/templates/ansible/roles/lamp.yml +6 -0
  53. data/lib/templates/ansible/roles/site/handlers/main.yml +3 -0
  54. data/lib/templates/ansible/roles/site/tasks/db.yml +23 -0
  55. data/lib/templates/ansible/roles/site/tasks/main.yml +16 -0
  56. data/lib/templates/ansible/roles/site/templates/vhost.conf.j2 +24 -0
  57. data/lib/templates/ansible/site.yml +10 -0
  58. data/lib/templates/roles/db/handlers/main.yml +2 -0
  59. data/lib/templates/roles/db/tasks/main.yml +24 -0
  60. data/lib/templates/roles/lamp/tasks/main.yml +36 -0
  61. data/lib/templates/roles/lamp.yml +6 -0
  62. data/lib/templates/roles/site/handlers/main.yml +3 -0
  63. data/lib/templates/roles/site/tasks/db.yml +23 -0
  64. data/lib/templates/roles/site/tasks/main.yml +16 -0
  65. data/lib/templates/roles/site/templates/vhost.conf.j2 +24 -0
  66. data/spec/hyrb/pipeline_spec.rb +91 -0
  67. data/spec/spec_helper.rb +5 -0
  68. metadata +295 -0
@@ -0,0 +1,35 @@
1
+ module Hyrb
2
+ module Models
3
+ module Cache
4
+ class Developers < Model
5
+ def initialize(data = nil)
6
+ super(File.join("cache", "developers"), data)
7
+ end
8
+
9
+ def serialize(data)
10
+ data.map(&:to_hash)
11
+ end
12
+
13
+ def deserialize(data)
14
+ data && data.map { |d| Developer.new(d) }
15
+ end
16
+ end
17
+
18
+ class DigitalOcean < Struct
19
+ define_keys %w( flavors images regions )
20
+
21
+ def initialize
22
+ super(File.join("cache", "digital_ocean"))
23
+ end
24
+ end
25
+
26
+ class Rackspace < Struct
27
+ define_keys %w( flavors images )
28
+
29
+ def initialize
30
+ super(File.join("cache", "rackspace"))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ module Hyrb
2
+ module Models
3
+ class Creds < Struct
4
+ define_keys %w( rackspace_api_key
5
+ rackspace_username
6
+ digital_ocean_api_key
7
+ digital_ocean_client_id
8
+ aws_access_key_id
9
+ aws_secret_access_key
10
+ github_org
11
+ github_api_key
12
+ hipchat_api_key
13
+ hipchat_user_id
14
+ google_client_id
15
+ google_client_secret
16
+ google_refresh_token
17
+ google_spreadsheet_key )
18
+
19
+ def initialize
20
+ super("creds")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module Hyrb
2
+ module Models
3
+ class Defaults < Struct
4
+ define_keys %w( project_path domain admin_email rackspace_region )
5
+
6
+ def initialize
7
+ super("defaults")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module Hyrb
2
+ module Models
3
+ class Developer
4
+ ROLE_MAP = {
5
+ 1 => :admin,
6
+ 2 => :developer,
7
+ 3 => :contractor,
8
+ 4 => :banned,
9
+ }
10
+
11
+ attr_accessor :name, :email, :role, :github_username, :keys, :digital_ocean_id
12
+
13
+ def initialize(hash = {})
14
+ hash.each do |k,v|
15
+ send("#{k}=", v)
16
+ end
17
+ end
18
+
19
+ def employee?
20
+ [:admin, :developer].include?(role)
21
+ end
22
+
23
+ def to_hash
24
+ Hash[%w(name email role github_username keys).map { |a| [a, send(a)] }]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ module Hyrb
2
+ module Models
3
+ class Environment < Struct
4
+ PROVIDERS = %w( digital_ocean rackspace )
5
+
6
+ attr_accessor :project
7
+
8
+ define_keys %w( name
9
+ host
10
+ domain
11
+ label
12
+ server_name
13
+ provider
14
+ server_id
15
+ deploy_user
16
+ base_path
17
+ relative_web_root
18
+ database_host
19
+ database_name
20
+ database_user
21
+ database_password
22
+ has_dns_record )
23
+
24
+ def initialize(project, name)
25
+ @name = name
26
+ super(File.join("projects", project.name, "project"))
27
+ self.project = project
28
+ self.name = name
29
+ end
30
+
31
+ def serialize(data)
32
+ project.environments[@name] = data
33
+ project.serialize(project.data)
34
+ end
35
+
36
+ def deserialize(data)
37
+ super(data["environments"][@name])
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module Hyrb
2
+ module Models
3
+ class Project < Struct
4
+ define_keys %w( name
5
+ title
6
+ description
7
+ github_org
8
+ repo_name
9
+ github_team
10
+ room_name
11
+ users
12
+ environments
13
+ jira_project
14
+ has_hipchat_hook )
15
+
16
+ def initialize(name)
17
+ super(File.join("projects", name, "project"))
18
+ self.name = name
19
+ data.environments ||= { }
20
+ end
21
+
22
+ def developers(all_developers)
23
+ users ? all_developers.select {|dev| users.include? dev.email } : []
24
+ end
25
+
26
+ def repo_path
27
+ "#{github_org}/#{repo_name}"
28
+ end
29
+
30
+ def ansible_path
31
+ File.join(File.dirname(path), "ansible")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ require 'tsort'
2
+
3
+ module Hyrb
4
+ class Pipeline
5
+ include TSort
6
+
7
+ attr_accessor :prev_tasks, :next_tasks, :current_task, :env
8
+
9
+ def self.rules
10
+ @@rules ||= {}
11
+ end
12
+
13
+ def initialize(task_class)
14
+ @task_class = task_class
15
+ @prev_tasks = []
16
+ @next_tasks = build_task_list
17
+ end
18
+
19
+ def run(env = {})
20
+ @env = env
21
+ while @current_task = @next_tasks.shift
22
+ task = @current_task.new(self)
23
+ info "--> Running #{@current_task}" if @env[:verbose]
24
+ task.run_before(@env)
25
+ task.run(@env)
26
+ @prev_tasks << @current_task
27
+ end
28
+ @env
29
+ end
30
+
31
+ def invoke(task_class)
32
+ pipeline = self.class.new(task_class)
33
+ pipeline.prev_tasks += self.prev_tasks
34
+ pipeline.next_tasks -= self.prev_tasks
35
+ pipeline.run(@env)
36
+ self.next_tasks -= pipeline.prev_tasks
37
+ end
38
+
39
+ def info(message)
40
+ puts message
41
+ end
42
+
43
+ def tsort_each_child(node, &block)
44
+ (self.class.rules[node] || []).each(&block)
45
+ end
46
+
47
+ def tsort_each_node(&block)
48
+ (self.class.rules || {}).each_key(&block)
49
+ end
50
+
51
+ def build_task_list(id_map={}, stack=[])
52
+ arr = []
53
+ each_strongly_connected_component_from(@task_class, id_map, stack) do |t|
54
+ if t.length == 1
55
+ arr << t.first
56
+ else
57
+ raise TSort::Cyclic.new("cyclic dependencies: #{t.inspect}")
58
+ end
59
+ end
60
+ arr
61
+ end
62
+ end
63
+ end
data/lib/hyrb/task.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'hyrb/pipeline'
2
+
3
+ module Hyrb
4
+ class Task
5
+ extend Forwardable
6
+
7
+ attr_reader :env, :pipeline
8
+
9
+ def_delegators :pipeline, :invoke, :say, :ask, :prompt, :edit, :option_list, :beep, :yes?, :no?
10
+
11
+ def self.depends(*args)
12
+ Commands::Pipeline.rules.merge!({self => args})
13
+ end
14
+
15
+ def self.prompt(hash_name, key, options = {})
16
+ self.prompts << [hash_name, key, options]
17
+ end
18
+
19
+ def self.prompts
20
+ @prompts ||= []
21
+ end
22
+
23
+ def initialize(pipeline)
24
+ @pipeline = pipeline
25
+ end
26
+
27
+ # TODO: run task after
28
+
29
+ def run(env)
30
+ end
31
+
32
+ def run_before(env)
33
+ self.class.prompts.each do |(hash_name, key, options)|
34
+ if options.is_a?(Hash) && options[:default].respond_to?(:call)
35
+ options[:default] = options[:default].call(env)
36
+ end
37
+
38
+ prompt "Please enter #{key}", env[hash_name], key, options
39
+ end
40
+ end
41
+ end
42
+
43
+ module Tasks
44
+ Hyrb.autoload_each self, "hyrb/tasks", %w( ansible creds defaults developers digital_ocean environment github google hipchat project provision rackspace )
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ module Hyrb
5
+ module Tasks
6
+ module Ansible
7
+ class Inject < Task
8
+ depends Environment::SetupExisting
9
+
10
+ def run(env)
11
+
12
+ end
13
+ end
14
+
15
+ class Init < Task
16
+ depends Environment::SetupExisting
17
+
18
+ def run(env)
19
+ end
20
+ end
21
+
22
+ class CreatePlaybookDir < Task
23
+ depends Init
24
+
25
+ def run(env)
26
+ # How to deal with enviornment vs project for ansible
27
+ if File.directory? env.project.ansible_path
28
+ say "Ansible dir exists in #{env.project.ansible_path}", :yellow
29
+ else
30
+ FileUtils.mkdir_p(env.project.ansible_path)
31
+ FileUtils.cp_r("#{TEMPLATE_PATH}/ansible/roles", "#{env.project.ansible_path}/roles")
32
+ say "Created ansible dir in #{env.project.ansible_path}", :green
33
+ end
34
+ end
35
+ end
36
+
37
+ class DestroyPlaybookDir < Task
38
+ depends Init
39
+
40
+ def run(env)
41
+ end
42
+ end
43
+
44
+ class CreatePlaybook < Task
45
+ depends CreatePlaybookDir, Environment::Deployment
46
+
47
+ def run(env)
48
+ host = Models::AnsibleHost.new(env.project, env.environment)
49
+ host.save!
50
+ say "Saved inventory file to #{host.filepath}", :green
51
+
52
+ site_playbook = Models::AnsibleSite.new(env.project, env.environment)
53
+ site_playbook.data.merge!({
54
+ project_name: env.project.name,
55
+ host: env.environment.host,
56
+ deploy_user: env.environment.deploy_user,
57
+ base_path: env.environment.base_path,
58
+ domain: env.environment.domain,
59
+ relative_web_root: env.environment.relative_web_root,
60
+ })
61
+
62
+ site_playbook.hosts = env.environment.label
63
+
64
+ if ! site_playbook.mysql_host && yes?("Does the project use a SQL database?")
65
+ invoke Environment::Database
66
+
67
+ site_playbook.data.merge!({
68
+ mysql_host: env.environment.database_host,
69
+ mysql_db: env.environment.database_name,
70
+ mysql_user: env.environment.database_user,
71
+ mysql_password: env.environment.database_password,
72
+ })
73
+
74
+ # TODO: add mysql role to playbook
75
+
76
+ site_playbook.vars_prompt = [{
77
+ "name" => "mysql_root_user",
78
+ "prompt" => "MySQL Root User",
79
+ "private" => false
80
+ },{
81
+ "name" => "mysql_root_password",
82
+ "prompt" => "MySQL Root Password"
83
+ }]
84
+ end
85
+
86
+ site_playbook.save!
87
+
88
+ say "Saved site playbook to #{site_playbook.filepath}", :green
89
+ say "Run it with", :green
90
+ say "\tcd #{env.project.ansible_path}", :green
91
+ say "\tansible-playbook -i $(which yaminv) #{env.environment.label}.yml", :green
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,39 @@
1
+ module Hyrb
2
+ module Tasks
3
+ module Creds
4
+ class Inject < Task
5
+ depends Defaults::Init
6
+
7
+ def run(env)
8
+ env.creds = Hyrb::Models::Creds.new
9
+ end
10
+ end
11
+
12
+ class Init < Task
13
+ depends Inject
14
+ end
15
+
16
+ class Show < Task
17
+ depends Init
18
+
19
+ def run(env)
20
+ say env.creds.data.to_hash.to_yaml
21
+ end
22
+ end
23
+
24
+ class Build < Task
25
+ depends Init
26
+
27
+ Hyrb::Models::Creds.keys.each { |key| prompt :creds, key, always: true }
28
+ end
29
+
30
+ class Edit < Task
31
+ depends Init
32
+
33
+ def run(env)
34
+ edit(env.creds.filepath)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ module Hyrb
2
+ module Tasks
3
+ module Defaults
4
+ class Inject < Task
5
+ def run(env)
6
+ env.defaults = Hyrb::Models::Defaults.new
7
+ end
8
+ end
9
+
10
+ class Init < Task
11
+ depends Inject
12
+
13
+ prompt :defaults, :domain, "hyfnrsx1.com"
14
+ end
15
+
16
+ class Show < Task
17
+ depends Defaults::Init
18
+
19
+ def run(env)
20
+ say env.defaults.data.to_hash.to_yaml
21
+ end
22
+ end
23
+
24
+ class Build < Task
25
+ depends Defaults::Init
26
+
27
+ Hyrb::Models::Defaults.keys.each { |key| prompt :defaults, key, always: true }
28
+
29
+ def run(env)
30
+ say "Credentials updated", :green
31
+ end
32
+ end
33
+
34
+ class Edit < Task
35
+ depends Defaults::Init
36
+
37
+ def run(env)
38
+ edit(env.defaults.filepath)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,120 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'google_doc_seed'
4
+ require 'json'
5
+ require 'csv'
6
+
7
+ module Hyrb
8
+ module Tasks
9
+ module Developers
10
+ class Inject < Task
11
+ def run(env)
12
+ env.developers = Hyrb::Models::Cache::Developers.new.data
13
+ end
14
+ end
15
+
16
+ class Init < Task
17
+ depends Inject
18
+
19
+ def run(env)
20
+ invoke(Download) if env.developers.blank?
21
+ end
22
+ end
23
+
24
+ class Show < Task
25
+ depends Init
26
+
27
+ def run(env)
28
+ say env.developers.map(&:to_hash).to_yaml
29
+ end
30
+ end
31
+
32
+ class Download < Task
33
+ depends Google::Init
34
+
35
+ def run(env)
36
+ response = Faraday.post "https://accounts.google.com/o/oauth2/token",
37
+ refresh_token: env.creds.google_refresh_token,
38
+ client_id: env.creds.google_client_id,
39
+ client_secret: env.creds.google_client_secret,
40
+ grant_type: "refresh_token"
41
+
42
+ access_token = JSON.parse(response.body)['access_token']
43
+ seeder = GoogleDocSeed.new(access_token)
44
+ csv_string = seeder.to_csv_string(env.creds.google_spreadsheet_key)
45
+
46
+ csv = CSV.parse(csv_string, GoogleCSVConverters::CSV_SETTINGS)
47
+
48
+ env.developers = Hyrb::Models::Cache::Developers.new
49
+ env.developers.data = csv.map do |row|
50
+ pks = []
51
+ while true
52
+ field = row.delete(:public_key)
53
+ if field.length > 1
54
+ pks << field[1] if field[1]
55
+ else
56
+ break
57
+ end
58
+ end
59
+
60
+ Hyrb::Models::Developer.new({
61
+ name: row[:name],
62
+ email: row[:email],
63
+ role: Hyrb::Models::Developer::ROLE_MAP[row[:role].to_i],
64
+ github_username: row[:github],
65
+ keys: pks,
66
+ }) if row[:role].to_i < 4 && row[:email]
67
+ end.compact
68
+
69
+ env.developers.save!
70
+ say "Developer data downloaded", :green
71
+ end
72
+ end
73
+
74
+ class AddToProject < Task
75
+ depends Init, Project::Init
76
+
77
+ def run(env)
78
+ added_devs = if env.project.users.try(:any?)
79
+ env.developers.select {|dev| env.project.users.include? dev.email }
80
+ else
81
+ env.developers.select {|dev| dev.role == :admin }
82
+ end
83
+
84
+ loop do
85
+ say "Devs with access"
86
+ list_devs(added_devs)
87
+
88
+ break unless yes? "Add more devs?"
89
+
90
+ dev_list = env.developers - added_devs
91
+ devs = *prompt_for_dev(dev_list)
92
+ added_devs += devs
93
+ end
94
+
95
+ env.project.users = added_devs.map(&:email)
96
+ env.project.save!
97
+ end
98
+
99
+ def list_devs(devs)
100
+ devs.each {|d| say "#{d.name} <#{d.email}>"}
101
+ end
102
+
103
+ def prompt_for_dev(devs)
104
+ option_list(devs) { |d, i| "#{i+1}: #{d.name} <#{d.email}>" }
105
+ end
106
+ end
107
+
108
+ module GoogleCSVConverters
109
+ BLANK_TO_NIL = ->(f) { f.empty? ? nil : f }
110
+ TRIM_WHITESPACE = ->(f) { f.respond_to?(:gsub) ? f.gsub(/\s+$/, '') : f }
111
+
112
+ CSV_SETTINGS = {
113
+ headers: true,
114
+ header_converters: :symbol,
115
+ converters: [BLANK_TO_NIL, TRIM_WHITESPACE],
116
+ }
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,84 @@
1
+ require 'fog/digitalocean'
2
+ require 'parallel'
3
+
4
+ module Hyrb
5
+ module Tasks
6
+ module DigitalOcean
7
+ class Inject < Task
8
+ depends Creds::Init
9
+
10
+ prompt :creds, :digital_ocean_api_key
11
+ prompt :creds, :digital_ocean_client_id
12
+
13
+ def run(env)
14
+ env.digital_ocean_client = Fog::Compute.new({
15
+ provider: 'DigitalOcean',
16
+ digitalocean_api_key: env.creds.digital_ocean_api_key,
17
+ digitalocean_client_id: env.creds.digital_ocean_client_id,
18
+ })
19
+ end
20
+ end
21
+
22
+ class Init < Task
23
+ depends Inject
24
+
25
+ def run(env)
26
+ # TODO: allow cache referesh to be forced
27
+ env.digital_ocean_cache = Hyrb::Models::Cache::DigitalOcean.new
28
+ env.digital_ocean_cache.class.keys.each do |key|
29
+ env.digital_ocean_cache[key] ||= env.digital_ocean_client.send(key).map(&:attributes)
30
+ end
31
+ env.digital_ocean_cache.save!
32
+ end
33
+ end
34
+
35
+ class SSHKeys < Task
36
+ depends Init, Developers::Init
37
+
38
+ def run(env)
39
+ env.digital_ocean_ssh_keys = Parallel.map(env.digital_ocean_client.ssh_keys.all, in_threads: 12) do |dokey|
40
+ env.digital_ocean_client.ssh_keys.get(dokey.id)
41
+ end
42
+
43
+ env.digital_ocean_ssh_keys.each do |k|
44
+ dev = env.developers.find { |dev| dev.keys.include? k.ssh_pub_key }
45
+ dev.digital_ocean_id = k.id if dev
46
+ end
47
+ end
48
+ end
49
+
50
+ class ShowSSHKeys < Task
51
+ depends SSHKeys
52
+
53
+ def run(env)
54
+ say env.digital_ocean_ssh_keys.map { |k| {name: k.name, id: k.id, ssh_key: k.ssh_pub_key} }.to_yaml
55
+ end
56
+ end
57
+
58
+ class SyncSSHKeys < Task
59
+ depends SSHKeys
60
+
61
+ def run(env)
62
+ env.digital_ocean_ssh_keys.each do |dokey|
63
+ unless key = env.developers.any? { |dev| dev.keys.include? dokey.ssh_pub_key }
64
+ say "Removing #{dokey.name} SSH key from Digital Ocean", :green
65
+ env.digital_ocean_client.ssh_keys.destroy(dokey.id)
66
+ end
67
+ end
68
+
69
+ env.developers.select { |d| d.keys.any? }.each do |dev|
70
+ if key = env.digital_ocean_ssh_keys.any? { |k| dev.keys.include? k.ssh_pub_key }
71
+ say "#{dev.name}'s keys already added to Digital Ocean", :yellow
72
+ else
73
+ dev.keys.each do |pubkey, i|
74
+ say "Adding pubkey for #{dev.name} to Digital Ocean", :green
75
+ env.digital_ocean_client.ssh_keys.create(name: dev.name, ssh_pub_key: pubkey)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end