knife-maas 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.md +19 -1
- data/README.md +2 -2
- data/knife-maas.gemspec +13 -10
- data/lib/chef/knife/maas_base.rb +65 -38
- data/lib/chef/knife/maas_server_acquire.rb +8 -27
- data/lib/chef/knife/maas_server_bootstrap.rb +45 -75
- data/lib/chef/knife/maas_server_commission.rb +23 -0
- data/lib/chef/knife/maas_server_delete.rb +29 -39
- data/lib/chef/knife/maas_server_list.rb +31 -58
- data/lib/chef/knife/maas_server_release.rb +29 -42
- data/lib/chef/knife/maas_server_show.rb +23 -0
- data/lib/chef/knife/maas_server_start.rb +8 -15
- data/lib/chef/knife/maas_server_stop.rb +8 -10
- data/lib/chef/maas/client.rb +143 -0
- data/lib/chef/maas/maas.rb +24 -0
- data/lib/chef/maas/version.rb +5 -0
- data/spec/knife/maas_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -1
- metadata +38 -5
- data/lib/chef/knife/maas.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6571f2695fd08992e7f747c0b5c4066490629e0a
|
4
|
+
data.tar.gz: 98e048add9f9c965a75b76f89b22709489e1444e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f793b7d791b9bd31303f2963a9427511a85c9dab14cb1bc8323424db7f906f61f65734ea8b559337f17a00a1ef2298d1cd99a1029d7c3c5bbd53563a4463094
|
7
|
+
data.tar.gz: 6b4585118138025cc7144be9a2c0093516535a57b83fa81ccf41bdffd912df84d7ada5bf721134ca09daf244da2c5b2e9bc4ac15035ea630a5c52d83f4f19bad
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Style/ClassAndModuleChildren:
|
2
|
+
Enabled: false
|
3
|
+
Style/GuardClause:
|
4
|
+
Enabled: false
|
5
|
+
Metrics/ClassLength:
|
6
|
+
Max: 200
|
7
|
+
Metrics/CyclomaticComplexity:
|
8
|
+
Max: 10
|
9
|
+
Metrics/PerceivedComplexity:
|
10
|
+
Max: 10
|
11
|
+
Metrics/MethodLength:
|
12
|
+
Max: 40
|
13
|
+
Metrics/AbcSize:
|
14
|
+
Max: 25
|
15
|
+
AllCops:
|
16
|
+
Exclude:
|
17
|
+
- 'Guardfile'
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,25 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [v2.
|
3
|
+
## [v2.1.0](https://github.com/chef-partners/knife-maas/tree/v2.1.0)
|
4
4
|
|
5
|
+
[Full Changelog](https://github.com/chef-partners/knife-maas/compare/v2.0.0...v2.1.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- sleeps are race-condition prone [\#15](https://github.com/chef-partners/knife-maas/issues/15)
|
10
|
+
|
11
|
+
**Closed issues:**
|
12
|
+
|
13
|
+
- API 1.0 no longer supported in maas 2.0 [\#21](https://github.com/chef-partners/knife-maas/issues/21)
|
14
|
+
- if statement is not needed. [\#17](https://github.com/chef-partners/knife-maas/issues/17)
|
15
|
+
- Ability to select or change the OS from knife [\#7](https://github.com/chef-partners/knife-maas/issues/7)
|
16
|
+
- Give the option of either hostname or systemid [\#5](https://github.com/chef-partners/knife-maas/issues/5)
|
17
|
+
|
18
|
+
**Merged pull requests:**
|
19
|
+
|
20
|
+
- Merge of 2.0 update [\#24](https://github.com/chef-partners/knife-maas/pull/24) ([tpmullan](https://github.com/tpmullan))
|
21
|
+
|
22
|
+
## [v2.0.0](https://github.com/chef-partners/knife-maas/tree/v2.0.0) (2017-03-16)
|
5
23
|
[Full Changelog](https://github.com/chef-partners/knife-maas/compare/v1.2.0...v2.0.0)
|
6
24
|
|
7
25
|
**Merged pull requests:**
|
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# knife-maas
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.org/
|
3
|
+
[![Build Status](https://travis-ci.org/chef-partners/knife-maas.svg?branch=master)](https://travis-ci.org/chef-partners/knife-maas)
|
4
4
|
|
5
5
|
This is the knife plugin to talk to [MAAS](http://maas.ubuntu.com/). This also assumes you have MAAS
|
6
6
|
configured with at least one user account. You'll need your API key as one of the
|
7
7
|
configuration options. There is a [MAAS cookbook](https://supermarket.chef.io/cookbooks/maas) that
|
8
8
|
this plugin has been tested against.
|
9
9
|
|
10
|
-
This also assumes you have >=
|
10
|
+
This also assumes you have >= 2.0.0 of MAAS installed, and >= [Chef](http://chef.io) 12 installed.
|
11
11
|
|
12
12
|
The version `2.0.0`+ of this knife plugin, requires version 2.0 of MAAS API.
|
13
13
|
|
data/knife-maas.gemspec
CHANGED
@@ -1,25 +1,28 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'chef/
|
4
|
+
require 'chef/maas/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
-
spec.version = Chef::
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
7
|
+
spec.name = 'knife-maas'
|
8
|
+
spec.version = Chef::Maas::VERSION
|
9
|
+
spec.authors = ['Chef Partner Engineering']
|
10
|
+
spec.email = ['jj@chef.io']
|
11
11
|
spec.summary = %q{A knife plugin to interact with MAAS}
|
12
12
|
spec.description = %q{A knife plugin to interact with MAAS}
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
13
|
+
spec.homepage = 'https://github.com/chef-partners/knife-maas'
|
14
|
+
spec.license = 'Apache2'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'chef', '~> 12.0'
|
22
|
+
spec.add_dependency 'oauth', '~> 0.4'
|
23
|
+
spec.add_dependency 'bson', '~> 3.0'
|
24
|
+
spec.add_dependency 'xml-simple', '~> 1.1'
|
20
25
|
|
21
|
-
spec.add_dependency "chef", "~> 12.0"
|
22
|
-
spec.add_dependency "oauth", "~> 0.4"
|
23
26
|
|
24
27
|
spec.add_development_dependency "bundler", "~> 1.7"
|
25
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/chef/knife/maas_base.rb
CHANGED
@@ -1,67 +1,94 @@
|
|
1
|
-
|
2
|
-
|
3
1
|
class Chef
|
4
2
|
class Knife
|
3
|
+
# Base Maas module with common methods
|
5
4
|
module MaasBase
|
6
|
-
|
7
5
|
def self.included(includer)
|
8
6
|
includer.class_eval do
|
9
|
-
|
10
7
|
deps do
|
11
|
-
require '
|
12
|
-
require '
|
13
|
-
require 'chef/
|
14
|
-
require 'chef/knife'
|
15
|
-
require 'readline'
|
8
|
+
require 'chef/maas/maas'
|
9
|
+
require 'chef/api_client'
|
10
|
+
require 'chef/node'
|
16
11
|
Chef::Knife.load_deps
|
17
12
|
end
|
18
|
-
# add common knife option flags here
|
19
13
|
end
|
20
14
|
end
|
21
15
|
|
22
|
-
def
|
16
|
+
def client
|
17
|
+
@client ||= begin
|
18
|
+
api_key = locate_config_value(:maas_api_key)
|
19
|
+
site_uri = locate_config_value(:maas_site)
|
20
|
+
::Maas::Client.new(api_key, site_uri)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def destroy_item(klass, name, type_name)
|
25
|
+
object = klass.load(name)
|
26
|
+
object.destroy
|
27
|
+
ui.warn("Deleted #{type_name} #{name}")
|
28
|
+
rescue Net::HTTPServerException
|
29
|
+
ui.warn("Could not find a #{type_name} named #{name} to delete!")
|
30
|
+
end
|
23
31
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
:signature_method => "PLAINTEXT"
|
30
|
-
})
|
32
|
+
def acquire_node(hostname: nil, zone: nil)
|
33
|
+
unless hostname || zone
|
34
|
+
ui.error 'Please specify either a zone or hostname'
|
35
|
+
exit 1
|
36
|
+
end
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
if !hostname.nil?
|
39
|
+
client.acquire_node(hostname: hostname)
|
40
|
+
elsif !zone.nil?
|
41
|
+
client.acquire_node(zone: zone)
|
42
|
+
end
|
37
43
|
end
|
38
44
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
prepare_access_token(token_key, token_secret, customer_key, customer_secret)
|
45
|
+
def print_node_status(response_json)
|
46
|
+
if response_json.is_a?(Hash) && code = response_json['status']
|
47
|
+
ui.info(ui.color(::Maas::NODE_STATUS_MAP[code.to_s][:status],
|
48
|
+
::Maas::NODE_STATUS_MAP[code.to_s][:color]))
|
49
|
+
end
|
50
|
+
response_json
|
46
51
|
end
|
47
52
|
|
48
53
|
def locate_config_value(key)
|
49
54
|
key = key.to_sym
|
50
|
-
Chef::Config[:knife][key]
|
55
|
+
config[key] || Chef::Config[:knife][key]
|
51
56
|
end
|
52
57
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
if locate_config_value(k).nil?
|
58
|
-
errors << "You did not provide a valid '#{k}' value."
|
58
|
+
def with_timeout(max_time = nil, &block)
|
59
|
+
if max_time
|
60
|
+
Timeout.timeout(max_time) do
|
61
|
+
block.call
|
59
62
|
end
|
63
|
+
else
|
64
|
+
block.call
|
65
|
+
end
|
66
|
+
rescue Timeout::Error
|
67
|
+
ui.error "Request took longer than #{max_time}"
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
|
71
|
+
def wait_with_dots(sleep_seconds = 1)
|
72
|
+
print('.') && sleep(sleep_seconds)
|
73
|
+
end
|
74
|
+
|
75
|
+
def ensure_system_id!
|
76
|
+
system_id = locate_config_value(:system_id) || name_args[0]
|
77
|
+
unless system_id
|
78
|
+
ui.error('You must provide the system id of the node')
|
79
|
+
exit 1
|
60
80
|
end
|
81
|
+
system_id
|
82
|
+
end
|
61
83
|
|
62
|
-
|
84
|
+
def ensure_chef_node_name!
|
85
|
+
hostname = locate_config_value(:hostname)
|
86
|
+
node_name = config[:chef_node_name] || hostname
|
87
|
+
unless node_name
|
88
|
+
ui.error ('You must provide the hostname or chef_node_name')
|
63
89
|
exit 1
|
64
90
|
end
|
91
|
+
node_name
|
65
92
|
end
|
66
93
|
end
|
67
94
|
end
|
@@ -3,44 +3,25 @@ require 'chef/knife/maas_base'
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class MaasServerAcquire < Knife
|
6
|
-
|
7
6
|
include Chef::Knife::MaasBase
|
8
7
|
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
banner "knife maas server acquire (options)"
|
8
|
+
banner 'knife maas server acquire (options)'
|
13
9
|
|
14
10
|
option :hostname,
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
short: '-h HOSTNAME',
|
12
|
+
long: '--hostname HOSTNAME',
|
13
|
+
description: 'The HOSTNAME inside of MAAS'
|
18
14
|
|
19
15
|
option :zone,
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
short: '-Z ZONE',
|
17
|
+
long: '--zone ZONE',
|
18
|
+
description: 'Bootstrap inside a ZONE inside of MAAS'
|
23
19
|
|
24
20
|
def run
|
25
|
-
|
26
21
|
hostname = locate_config_value(:hostname)
|
27
22
|
zone = locate_config_value(:zone)
|
28
|
-
|
29
|
-
if (!hostname.nil? && !zone.nil?)
|
30
|
-
puts "\nPlease only use one of these options, zone or hostname"
|
31
|
-
exit 1
|
32
|
-
elsif !hostname.nil?
|
33
|
-
response = access_token.request(:post, "/nodes/", {'op'=> 'acquire','name' => "#{hostname}"})
|
34
|
-
elsif !zone.nil?
|
35
|
-
response = access_token.request(:post, "/nodes/", {'op'=> 'acquire','zone' => "#{zone}"})
|
36
|
-
else
|
37
|
-
response = access_token.request(:post, "/nodes/?op=acquire")
|
38
|
-
end
|
39
|
-
|
40
|
-
hostname = JSON.parse(response.body)["hostname"]
|
41
|
-
puts "Acquiring #{hostname} under your account now...."
|
23
|
+
print_node_status(acquire_node(hostname: hostname, zone: zone))
|
42
24
|
end
|
43
|
-
|
44
25
|
end
|
45
26
|
end
|
46
27
|
end
|
@@ -3,7 +3,6 @@ require 'chef/knife/maas_base'
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class MaasServerBootstrap < Knife
|
6
|
-
|
7
6
|
include Chef::Knife::MaasBase
|
8
7
|
|
9
8
|
deps do
|
@@ -11,100 +10,74 @@ class Chef
|
|
11
10
|
Chef::Knife::Bootstrap.load_deps
|
12
11
|
end
|
13
12
|
|
14
|
-
banner
|
13
|
+
banner 'knife maas server bootstrap (options)'
|
15
14
|
|
16
15
|
option :hostname,
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
short: '-h HOSTNAME',
|
17
|
+
long: '--hostname HOSTNAME',
|
18
|
+
description: 'The HOSTNAME inside of MAAS'
|
20
19
|
|
21
20
|
option :template_file,
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
long: '--template-file TEMPLATE',
|
22
|
+
description: 'Full path to location of template to use',
|
23
|
+
proc: proc { |t| Chef::Config[:knife][:template_file] = t },
|
24
|
+
default: false
|
26
25
|
|
27
26
|
option :run_list,
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
short: '-r RUN_LIST',
|
28
|
+
long: '--run-list RUN_LIST',
|
29
|
+
description: 'Comma separated list of roles/recipes to apply',
|
30
|
+
proc: -> (o) { o.split(/[\s,]+/) },
|
31
|
+
default: []
|
33
32
|
|
34
33
|
option :zone,
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
short: '-Z ZONE',
|
35
|
+
long: '--zone ZONE',
|
36
|
+
description: 'Bootstrap inside a ZONE inside of MAAS'
|
37
|
+
|
38
|
+
option :bootstrap_timeout,
|
39
|
+
short: '-T TIMEOUT',
|
40
|
+
long: '--bootstrap-timeout',
|
41
|
+
description: 'How long to wait for the Server to initially boot',
|
42
|
+
default: 120
|
38
43
|
|
39
44
|
def run
|
45
|
+
node = acquire_node(hostname: locate_config_value(:hostname),
|
46
|
+
zone: locate_config_value(:zone)
|
47
|
+
)
|
40
48
|
|
41
|
-
|
42
|
-
zone = locate_config_value(:zone)
|
49
|
+
system_id = node['system_id']
|
43
50
|
|
44
|
-
|
45
|
-
puts "\nPlease only use one of these options, zone or hostname"
|
46
|
-
exit 1
|
47
|
-
elsif !hostname.nil?
|
48
|
-
response = access_token.request(:post, "/nodes/", {'op'=> 'acquire','name' => "#{hostname}"})
|
49
|
-
elsif !zone.nil?
|
50
|
-
response = access_token.request(:post, "/nodes/", {'op'=> 'acquire','zone' => "#{zone}"})
|
51
|
-
else
|
52
|
-
response = access_token.request(:post, "/nodes/?op=acquire")
|
53
|
-
end
|
51
|
+
puts "Acquiring #{node["hostname"]} under your account now.\n"
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
puts "Acquiring #{hostname} under your account now...."
|
59
|
-
|
60
|
-
# hack to ensure the node have had time to spin up
|
61
|
-
print(".")
|
62
|
-
sleep 30
|
63
|
-
print(".")
|
64
|
-
|
65
|
-
response = access_token.request(:post, "/nodes/#{system_id}/?op=start")
|
66
|
-
puts "\nStarting up #{hostname} now...."
|
67
|
-
|
68
|
-
# hack to ensure the nodes have had time to spin up
|
69
|
-
print(".")
|
70
|
-
sleep 30
|
71
|
-
print(".")
|
72
|
-
|
73
|
-
server = JSON.parse(system_info.body)["hostname"]
|
74
|
-
netboot = JSON.parse(system_info.body)["netboot"]
|
75
|
-
power_state = JSON.parse(system_info.body)["power_state"]
|
76
|
-
|
77
|
-
until ((netboot == false) && (power_state == "on") ) do
|
78
|
-
print(".")
|
79
|
-
sleep @initial_sleep_delay ||= 10
|
80
|
-
system_info = access_token.request(:get, "/nodes/#{system_id}/")
|
81
|
-
netboot = JSON.parse(system_info.body)["netboot"]
|
82
|
-
power_state = JSON.parse(system_info.body)["power_state"]
|
53
|
+
with_timeout(60) do
|
54
|
+
# wait until node is in 'Allocated' state
|
55
|
+
wait_with_dots until client.list_node(system_id)['status'].to_s == '10'
|
83
56
|
end
|
84
57
|
|
85
|
-
|
58
|
+
puts "Deploying #{node["hostname"]}\n"
|
59
|
+
client.deploy_node(system_id)
|
86
60
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
61
|
+
with_timeout(60000) do
|
62
|
+
# wait until node is in 'Allocated' state
|
63
|
+
wait_with_dots until client.list_node(system_id)['status'].to_s == '6'
|
64
|
+
end
|
91
65
|
|
92
|
-
|
93
|
-
|
94
|
-
puts("connected and done")
|
95
|
-
}
|
66
|
+
system_info = client.list_node(system_id)
|
67
|
+
bootstrap_ip_address = system_info['ip_addresses'][0]
|
96
68
|
|
97
|
-
|
69
|
+
with_timeout(config[:bootstrap_timeout]) do
|
70
|
+
wait_with_dots(5) until tcp_test_ssh(bootstrap_ip_address)
|
71
|
+
end
|
98
72
|
|
99
|
-
case
|
100
|
-
when
|
101
|
-
user =
|
73
|
+
case system_info['osystem']
|
74
|
+
when 'centos'
|
75
|
+
user = 'cloud-user'
|
102
76
|
else
|
103
|
-
user =
|
77
|
+
user = 'ubuntu'
|
104
78
|
end
|
105
79
|
|
106
80
|
bootstrap_for_node(server, bootstrap_ip_address, user).run
|
107
|
-
|
108
81
|
end
|
109
82
|
|
110
83
|
def bootstrap_for_node(server, bootstrap_ip_address, user)
|
@@ -140,10 +113,8 @@ class Chef
|
|
140
113
|
rescue Errno::EPERM
|
141
114
|
false
|
142
115
|
rescue Errno::ECONNREFUSED
|
143
|
-
sleep 2
|
144
116
|
false
|
145
117
|
rescue Errno::EHOSTUNREACH
|
146
|
-
sleep 2
|
147
118
|
false
|
148
119
|
rescue Errno::ENETUNREACH
|
149
120
|
sleep 2
|
@@ -151,7 +122,6 @@ class Chef
|
|
151
122
|
ensure
|
152
123
|
tcp_socket && tcp_socket.close
|
153
124
|
end
|
154
|
-
|
155
125
|
end
|
156
126
|
end
|
157
127
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'chef/knife/maas_base'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
class MaasServerCommission < Knife
|
6
|
+
include Chef::Knife::MaasBase
|
7
|
+
|
8
|
+
banner 'knife maas server commission (options)'
|
9
|
+
|
10
|
+
option :system_id,
|
11
|
+
short: '-s SYSTEM_ID',
|
12
|
+
long: '--system-id SYSTEM_ID',
|
13
|
+
description: 'The System ID inside of MaaS'
|
14
|
+
|
15
|
+
def run
|
16
|
+
unless system_id = locate_config_value(:system_id) || name_args[0]
|
17
|
+
ui.error('You must provide the system id of the node')
|
18
|
+
end
|
19
|
+
print_node_status(client.commission_node(system_id))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,59 +1,49 @@
|
|
1
1
|
require 'chef/knife/maas_base'
|
2
|
-
require 'chef/node'
|
3
|
-
require 'chef/api_client'
|
4
2
|
|
5
3
|
class Chef
|
6
4
|
class Knife
|
7
5
|
class MaasServerDelete < Knife
|
8
|
-
|
9
6
|
include Chef::Knife::MaasBase
|
10
7
|
|
11
|
-
banner
|
8
|
+
banner 'knife maas server delete (options)'
|
12
9
|
|
13
10
|
option :hostname,
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
short: '-h HOSTNAME',
|
12
|
+
long: '--hostname HOSTNAME',
|
13
|
+
description: 'The HOSTNAME inside of MaaS'
|
17
14
|
|
18
15
|
option :system_id,
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
short: '-s SYSTEM_ID',
|
17
|
+
long: '--system-id SYSTEM_ID',
|
18
|
+
description: 'The System ID inside of MaaS'
|
22
19
|
|
23
20
|
option :purge,
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
object.destroy
|
34
|
-
ui.warn("Deleted #{type_name} #{name}")
|
35
|
-
rescue Net::HTTPServerException
|
36
|
-
ui.warn("Could not find a #{type_name} named #{name} to delete!")
|
37
|
-
end
|
38
|
-
end
|
21
|
+
short: '-P',
|
22
|
+
long: '--purge',
|
23
|
+
boolean: true,
|
24
|
+
default: false,
|
25
|
+
description: <<-EOS.gsub(/^ {15}/, '').gsub(/\n/, ' ')
|
26
|
+
Destroy corresponding node and client on the Chef Server, in
|
27
|
+
addition to destroying the MaaS node itself. Assumes node and
|
28
|
+
client have the same name as the server.
|
29
|
+
EOS
|
39
30
|
|
40
31
|
def run
|
41
|
-
system_id =
|
42
|
-
|
43
|
-
|
44
|
-
if
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
32
|
+
system_id = ensure_system_id!
|
33
|
+
node_name = ensure_chef_node_name! if config[:purge]
|
34
|
+
|
35
|
+
if print_node_status(client.delete_node(system_id))
|
36
|
+
if config[:purge]
|
37
|
+
destroy_item(Chef::Node, node_name, 'node')
|
38
|
+
destroy_item(Chef::ApiClient, node_name, 'client')
|
39
|
+
else
|
40
|
+
ui.warn <<-EOS.gsub(/^ {14}/, '').gsub(/\n/, ' ')
|
41
|
+
The corresponding node and client for #{node_name || system_id}
|
42
|
+
were not deleted and remain registered with the Chef Server
|
43
|
+
EOS
|
44
|
+
end
|
50
45
|
end
|
51
|
-
|
52
|
-
response = access_token.request(:post, "/nodes/#{system_id}/?op=delete")
|
53
|
-
puts "Nuking #{system_id} From Orbit now...."
|
54
46
|
end
|
55
|
-
|
56
|
-
|
57
47
|
end
|
58
48
|
end
|
59
49
|
end
|
@@ -3,13 +3,11 @@ require 'chef/knife/maas_base'
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class MaasServerList < Knife
|
6
|
-
|
7
6
|
include Chef::Knife::MaasBase
|
8
7
|
|
9
|
-
banner
|
8
|
+
banner 'knife maas server list'
|
10
9
|
|
11
10
|
def run
|
12
|
-
|
13
11
|
server_list = [
|
14
12
|
ui.color('System ID', :bold),
|
15
13
|
ui.color('Status', :bold),
|
@@ -19,63 +17,38 @@ class Chef
|
|
19
17
|
ui.color('IP Address', :bold)
|
20
18
|
]
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
# ALLOCATED = 10
|
53
|
-
# FAILED_DEPLOYMENT = 11
|
54
|
-
# RELEASING = 12
|
55
|
-
# FAILED_RELEASING = 13
|
56
|
-
# DISK_ERASING = 14
|
57
|
-
# FAILED_DISK_ERASING = 15
|
58
|
-
server_list << begin
|
59
|
-
power_state = server['power_state'].to_s
|
60
|
-
case power_state
|
61
|
-
when "off"
|
62
|
-
ui.color(power_state, :red)
|
63
|
-
when "on"
|
64
|
-
ui.color(power_state, :green)
|
65
|
-
else
|
66
|
-
ui.color(power_state, :yellow)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
server_list << server['hostname'].to_s
|
70
|
-
server_list << server['owner'].to_s
|
71
|
-
server_list << server['ip_addresses'].to_s
|
72
|
-
|
20
|
+
list = client.list_nodes
|
21
|
+
|
22
|
+
if list.is_a? Array
|
23
|
+
list.sort_by { |h| h['system_id'] }.each do |server|
|
24
|
+
#Chef::Log.debug("Server: #{server.to_yaml}")
|
25
|
+
|
26
|
+
server_list << server['system_id'].to_s
|
27
|
+
|
28
|
+
status = server['substatus'] || server['status'] || '-1'
|
29
|
+
server_list << ui.color(::Maas::NODE_STATUS_MAP[status.to_s][:status],
|
30
|
+
::Maas::NODE_STATUS_MAP[status.to_s][:color])
|
31
|
+
|
32
|
+
server_list << begin
|
33
|
+
power_state = server['power_state'].to_s
|
34
|
+
case power_state
|
35
|
+
when 'off'
|
36
|
+
ui.color(power_state, :red)
|
37
|
+
when 'on'
|
38
|
+
ui.color(power_state, :green)
|
39
|
+
else
|
40
|
+
ui.color(power_state, :yellow)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
server_list << server['hostname'].to_s
|
44
|
+
server_list << server['owner'].to_s
|
45
|
+
server_list << server['ip_addresses'].to_s
|
46
|
+
end
|
47
|
+
ui.info(ui.list(server_list, :uneven_columns_across, 6))
|
48
|
+
else
|
49
|
+
ui.info(list)
|
73
50
|
end
|
74
|
-
puts ui.list(server_list, :uneven_columns_across, 6)
|
75
|
-
|
76
51
|
end
|
77
|
-
|
78
|
-
|
79
52
|
end
|
80
53
|
end
|
81
54
|
end
|
@@ -1,62 +1,49 @@
|
|
1
1
|
require 'chef/knife/maas_base'
|
2
|
-
require 'chef/node'
|
3
|
-
require 'chef/api_client'
|
4
2
|
|
5
3
|
class Chef
|
6
4
|
class Knife
|
7
5
|
class MaasServerRelease < Knife
|
8
|
-
|
9
6
|
include Chef::Knife::MaasBase
|
10
7
|
|
11
|
-
banner
|
8
|
+
banner 'knife maas server release (options)'
|
12
9
|
|
13
10
|
option :hostname,
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
short: '-h HOSTNAME',
|
12
|
+
long: '--hostname HOSTNAME',
|
13
|
+
description: 'The HOSTNAME inside of MAAS'
|
17
14
|
|
18
15
|
option :system_id,
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
short: '-s SYSTEM_ID',
|
17
|
+
long: '--system-id SYSTEM_ID',
|
18
|
+
description: 'The System ID inside of MAAS'
|
22
19
|
|
23
20
|
option :purge,
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
object.destroy
|
34
|
-
ui.warn("Deleted #{type_name} #{name}")
|
35
|
-
rescue Net::HTTPServerException
|
36
|
-
ui.warn("Could not find a #{type_name} named #{name} to delete!")
|
37
|
-
end
|
38
|
-
end
|
21
|
+
short: '-P',
|
22
|
+
long: '--purge',
|
23
|
+
boolean: true,
|
24
|
+
default: false,
|
25
|
+
description: <<-EOS.gsub(/^ {15}/, '').gsub(/\n/, ' ')
|
26
|
+
Destroy corresponding node and client on the Chef Server, in
|
27
|
+
addition to releasing the MAAS node itself. Assumes node and
|
28
|
+
client have the same name as the server.
|
29
|
+
EOS
|
39
30
|
|
40
31
|
def run
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
32
|
+
system_id = ensure_system_id!
|
33
|
+
node_name = ensure_chef_node_name! if config[:purge]
|
34
|
+
|
35
|
+
if print_node_status(client.release_node(system_id))
|
36
|
+
if config[:purge]
|
37
|
+
destroy_item(Chef::Node, node_name, 'node')
|
38
|
+
destroy_item(Chef::ApiClient, node_name, 'client')
|
39
|
+
else
|
40
|
+
ui.warn <<-EOS.gsub(/^ {14}/, '').gsub(/\n/, ' ')
|
41
|
+
The corresponding node and client for #{node_name || system_id}
|
42
|
+
were not deleted and remain registered with the Chef Server
|
43
|
+
EOS
|
44
|
+
end
|
53
45
|
end
|
54
|
-
|
55
|
-
response = access_token.request(:post, "/nodes/#{system_id}/?op=release")
|
56
|
-
puts "Releasing #{system_id} back to MAAS now...."
|
57
46
|
end
|
58
|
-
|
59
|
-
|
60
47
|
end
|
61
48
|
end
|
62
49
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'chef/knife/maas_base'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
class MaasServerShow < Knife
|
6
|
+
include Chef::Knife::MaasBase
|
7
|
+
|
8
|
+
banner 'knife maas server show $NODE (options)'
|
9
|
+
|
10
|
+
option :system_id,
|
11
|
+
short: '-s SYSTEM_ID',
|
12
|
+
long: '--system-id SYSTEM_ID',
|
13
|
+
description: 'The System ID inside of MaaS'
|
14
|
+
|
15
|
+
def run
|
16
|
+
unless system_id = locate_config_value(:system_id) || name_args[0]
|
17
|
+
ui.error('You must provide the system id of the node')
|
18
|
+
end
|
19
|
+
ui.info(Chef::JSONCompat.to_json_pretty(client.show_node(system_id)))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -3,28 +3,21 @@ require 'chef/knife/maas_base'
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class MaasServerStart < Knife
|
6
|
-
|
7
6
|
include Chef::Knife::MaasBase
|
8
7
|
|
9
|
-
|
10
|
-
require 'chef/knife/bootstrap'
|
11
|
-
Chef::Knife::Bootstrap.load_deps
|
12
|
-
end
|
13
|
-
|
14
|
-
banner "knife maas server start (options)"
|
8
|
+
banner 'knife maas server start (options)'
|
15
9
|
|
16
10
|
option :system_id,
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
short: '-s SYSTEM_ID',
|
12
|
+
long: '--system-id SYSTEM_ID',
|
13
|
+
description: 'The System ID inside of MaaS'
|
20
14
|
|
21
15
|
def run
|
22
|
-
system_id = locate_config_value(:system_id)
|
23
|
-
|
24
|
-
|
16
|
+
unless system_id = locate_config_value(:system_id) || name_args[0]
|
17
|
+
ui.error('You must provide the system id of the node')
|
18
|
+
end
|
19
|
+
print_node_status(client.start_node(system_id))
|
25
20
|
end
|
26
|
-
|
27
|
-
|
28
21
|
end
|
29
22
|
end
|
30
23
|
end
|
@@ -3,23 +3,21 @@ require 'chef/knife/maas_base'
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class MaasServerStop < Knife
|
6
|
-
|
7
6
|
include Chef::Knife::MaasBase
|
8
7
|
|
9
|
-
banner
|
8
|
+
banner 'knife maas server stop (options)'
|
10
9
|
|
11
10
|
option :system_id,
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
short: '-s SYSTEM_ID',
|
12
|
+
long: '--system-id SYSTEM_ID',
|
13
|
+
description: 'The System ID inside of MaaS'
|
15
14
|
|
16
15
|
def run
|
17
|
-
system_id = locate_config_value(:system_id)
|
18
|
-
|
19
|
-
|
16
|
+
unless system_id = locate_config_value(:system_id) || name_args[0]
|
17
|
+
ui.error('You must provide the system id of the node')
|
18
|
+
end
|
19
|
+
print_node_status(client.stop_node(system_id))
|
20
20
|
end
|
21
|
-
|
22
|
-
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
require 'oauth/signature/plaintext'
|
3
|
+
require 'chef/json_compat'
|
4
|
+
require 'chef/knife'
|
5
|
+
require 'readline'
|
6
|
+
require 'timeout'
|
7
|
+
require 'forwardable'
|
8
|
+
require 'bson'
|
9
|
+
require 'xmlsimple'
|
10
|
+
|
11
|
+
module Maas
|
12
|
+
class Client
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
attr_reader :api_key, :site_uri, :access_token, :ui
|
16
|
+
|
17
|
+
def initialize(api_key, site_uri)
|
18
|
+
@api_key = api_key
|
19
|
+
@site_uri = site_uri
|
20
|
+
@access_token = access_token
|
21
|
+
@ui = Chef::Knife.ui
|
22
|
+
end
|
23
|
+
|
24
|
+
def_delegators :@access_token, :get, :put, :post, :delete, :request
|
25
|
+
|
26
|
+
def acquire_node(hostname: nil, zone: nil)
|
27
|
+
if hostname && zone
|
28
|
+
ui.error 'Please specify either a zone or hostname'
|
29
|
+
elsif !hostname && !zone
|
30
|
+
ui.error 'Please specify a zone or hostname'
|
31
|
+
end
|
32
|
+
|
33
|
+
if hostname
|
34
|
+
response = post('/machines/', 'op' => 'allocate', 'name' => "#{hostname}")
|
35
|
+
elsif zone
|
36
|
+
response = post('/machines/', 'op' => 'allocate', 'zone' => "#{zone}")
|
37
|
+
end
|
38
|
+
|
39
|
+
parse_response(response)
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: Maybe use method_missing to dynamically build these helpers
|
43
|
+
|
44
|
+
def delete_node(system_id)
|
45
|
+
parse_response(delete("/nodes/#{system_id}/"))
|
46
|
+
end
|
47
|
+
|
48
|
+
def show_node(system_id)
|
49
|
+
bson_parse(get("/nodes/#{system_id}/?op=details"))
|
50
|
+
end
|
51
|
+
|
52
|
+
def list_node(system_id)
|
53
|
+
list = parse_response(get("/nodes/?id=#{system_id}"))
|
54
|
+
if list.is_a? Array
|
55
|
+
return list.first
|
56
|
+
else
|
57
|
+
return Hash.new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def list_nodes
|
62
|
+
parse_response(get('/nodes/'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def release_node(system_id)
|
66
|
+
parse_response(post("/machines/#{system_id}/?op=release"))
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_node(system_id)
|
70
|
+
parse_response(post("/machines/#{system_id}/?op=power_on"))
|
71
|
+
end
|
72
|
+
|
73
|
+
def deploy_node(system_id)
|
74
|
+
parse_response(post("/machines/#{system_id}/?op=deploy"))
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def commission_node(system_id)
|
79
|
+
parse_response(post("/machines/#{system_id}/?op=commission"))
|
80
|
+
end
|
81
|
+
|
82
|
+
def stop_node(system_id)
|
83
|
+
parse_response(post("/machines/#{system_id}/?op=power_off"))
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def access_token
|
89
|
+
consumer_key, token_key, token_secret = api_key.split(':')
|
90
|
+
consumer = OAuth::Consumer.new(consumer_key,
|
91
|
+
'',
|
92
|
+
site: "#{site_uri}api/2.0",
|
93
|
+
scheme: :header,
|
94
|
+
signature_method: 'PLAINTEXT'
|
95
|
+
)
|
96
|
+
OAuth::AccessToken.new(consumer,
|
97
|
+
token_key,
|
98
|
+
token_secret
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_response(response)
|
103
|
+
case response
|
104
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
105
|
+
parse_response_json(response)
|
106
|
+
else
|
107
|
+
ui.error(parse_response_json(response))
|
108
|
+
exit 1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_response_json(response)
|
113
|
+
Chef::JSONCompat.parse(response.body)
|
114
|
+
rescue Chef::Exceptions::JSON::ParseError
|
115
|
+
response.body
|
116
|
+
end
|
117
|
+
|
118
|
+
def bson_parse(response)
|
119
|
+
decoded = {}
|
120
|
+
Hash.from_bson(StringIO.new(response.body)).each_pair do |type, binary|
|
121
|
+
decoded[type] = XmlSimple.xml_in(binary.data)
|
122
|
+
end
|
123
|
+
decoded
|
124
|
+
end
|
125
|
+
|
126
|
+
def with_timeout(max_time = nil, &block)
|
127
|
+
if max_time
|
128
|
+
Timeout.timeout(max_time) do
|
129
|
+
block.call
|
130
|
+
end
|
131
|
+
else
|
132
|
+
block.call
|
133
|
+
end
|
134
|
+
rescue Timeout::Error
|
135
|
+
ui.error "Request took longer than #{max_time}"
|
136
|
+
exit 1
|
137
|
+
end
|
138
|
+
|
139
|
+
def wait_with_dots(sleep_seconds = 1)
|
140
|
+
print('.') && sleep(sleep_seconds)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'chef/maas/version'
|
2
|
+
require 'chef/maas/client'
|
3
|
+
|
4
|
+
module Maas
|
5
|
+
NODE_STATUS_MAP = {
|
6
|
+
'-1' => { color: :red, status: 'unknown' },
|
7
|
+
'0' => { color: :yellow, status: 'new' },
|
8
|
+
'1' => { color: :yellow, status: 'commissioning' },
|
9
|
+
'2' => { color: :red, status: 'failed_commissioning' },
|
10
|
+
'3' => { color: :red, status: 'missing' },
|
11
|
+
'4' => { color: :green, status: 'ready' },
|
12
|
+
'5' => { color: :yellow, status: 'reserved' },
|
13
|
+
'6' => { color: :green, status: 'deployed' },
|
14
|
+
'7' => { color: :yellow, status: 'retired' },
|
15
|
+
'8' => { color: :red, status: 'broken' },
|
16
|
+
'9' => { color: :yellow, status: 'deploying' },
|
17
|
+
'10' => { color: :yellow, status: 'allocated' },
|
18
|
+
'11' => { color: :red, status: 'failed_deployment' },
|
19
|
+
'12' => { color: :yellow, status: 'releasing' },
|
20
|
+
'13' => { color: :red, status: 'failed_releasing' },
|
21
|
+
'14' => { color: :yellow, status: 'disk_erasing' },
|
22
|
+
'15' => { color: :red, status: 'failed_disk_erasing' }
|
23
|
+
}
|
24
|
+
end
|
data/spec/knife/maas_spec.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knife-maas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Chef Partner Engineering
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chef
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bson
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: xml-simple
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: bundler
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -103,6 +131,7 @@ extra_rdoc_files: []
|
|
103
131
|
files:
|
104
132
|
- ".gitignore"
|
105
133
|
- ".rspec"
|
134
|
+
- ".rubocop.yml"
|
106
135
|
- ".travis.yml"
|
107
136
|
- CHANGELOG.md
|
108
137
|
- Gemfile
|
@@ -110,19 +139,23 @@ files:
|
|
110
139
|
- README.md
|
111
140
|
- Rakefile
|
112
141
|
- knife-maas.gemspec
|
113
|
-
- lib/chef/knife/maas.rb
|
114
142
|
- lib/chef/knife/maas/version.rb
|
115
143
|
- lib/chef/knife/maas_base.rb
|
116
144
|
- lib/chef/knife/maas_server_acquire.rb
|
117
145
|
- lib/chef/knife/maas_server_bootstrap.rb
|
146
|
+
- lib/chef/knife/maas_server_commission.rb
|
118
147
|
- lib/chef/knife/maas_server_delete.rb
|
119
148
|
- lib/chef/knife/maas_server_list.rb
|
120
149
|
- lib/chef/knife/maas_server_release.rb
|
150
|
+
- lib/chef/knife/maas_server_show.rb
|
121
151
|
- lib/chef/knife/maas_server_start.rb
|
122
152
|
- lib/chef/knife/maas_server_stop.rb
|
153
|
+
- lib/chef/maas/client.rb
|
154
|
+
- lib/chef/maas/maas.rb
|
155
|
+
- lib/chef/maas/version.rb
|
123
156
|
- spec/knife/maas_spec.rb
|
124
157
|
- spec/spec_helper.rb
|
125
|
-
homepage:
|
158
|
+
homepage: https://github.com/chef-partners/knife-maas
|
126
159
|
licenses:
|
127
160
|
- Apache2
|
128
161
|
metadata: {}
|