knife-maas 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|
-
[](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: {}
|