diff options
author | Steven Danna <steve@opscode.com> | 2014-11-19 13:49:27 +0000 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2015-01-25 14:06:51 -0800 |
commit | 875b90d97a5c65efbda7853b4db25c18e808bd61 (patch) | |
tree | 51d8ea95290afde750d6b40e24cf254b3c5a2aa1 | |
parent | f923755a505c31745f6df14e201149128ba4ebec (diff) | |
download | chef-875b90d97a5c65efbda7853b4db25c18e808bd61.tar.gz |
Add Chef::Org model class
Chef Server 12 added multi-tenancy to the open source Chef Server. An
organization is the Chef Servers name for a tenant. The Chef::Org
class provides a model of an organization to facilitate writting knife
commands that can interact with (create, destroy, list) Chef
organizations.
This implementation is copied directly from knife-opc, the knife
plugin currently in use by chef-server-ctl:
https://github.com/opscode/knife-opc
-rw-r--r-- | lib/chef/org.rb | 143 | ||||
-rw-r--r-- | spec/unit/org_spec.rb | 196 |
2 files changed, 339 insertions, 0 deletions
diff --git a/lib/chef/org.rb b/lib/chef/org.rb new file mode 100644 index 0000000000..bbc4263835 --- /dev/null +++ b/lib/chef/org.rb @@ -0,0 +1,143 @@ +require 'chef/json_compat' +require 'chef/mixin/params_validate' +require 'chef/rest' + +class Chef + class Org + + include Chef::Mixin::ParamsValidate + + def initialize(name='') + @name = name + @full_name = '' + # The Chef API returns the private key of the validator + # client on create + @private_key = nil + @guid = nil + end + + def chef_rest + @chef_rest ||= Chef::REST.new(Chef::Config[:chef_server_root]) + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def full_name(arg=nil) + set_or_return(:full_name, + arg, :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, + arg, :kind_of => String) + end + + def guid(arg=nil) + set_or_return(:guid, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "full_name" => @full_name + } + result["private_key"] = @private_key if @private_key + result["guid"] = @guid if @guid + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def create + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.post_rest("organizations", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def update + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.put_rest("organizations/#{name}", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def destroy + chef_rest.delete_rest("organizations/#{@name}") + end + + def save + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update + else + raise e + end + end + end + + def associate_user(username) + request_body = {:user => username} + response = chef_rest.post_rest "organizations/#{@name}/association_requests", request_body + association_id = response["uri"].split("/").last + chef_rest.put_rest "users/#{username}/association_requests/#{association_id}", { :response => 'accept' } + end + + def dissociate_user(username) + chef_rest.delete_rest "organizations/#{name}/users/#{username}" + end + + def add_user_to_group(groupname, username) + group = chef_rest.get_rest "organizations/#{name}/groups/#{groupname}" + body_hash = { + :groupname => "#{groupname}", + :actors => { + "users" => group["actors"].concat([username]), + "groups" => group["groups"] + } + } + chef_rest.put_rest "organizations/#{name}/groups/#{groupname}", body_hash + end + + # Class methods + def self.from_hash(org_hash) + org = Chef::Org.new + org.name org_hash['name'] + org.full_name org_hash['full_name'] + org.private_key org_hash['private_key'] if org_hash.key?('private_key') + org.guid org_hash['guid'] if org_hash.key?('guid') + org + end + + def self.from_json(json) + Chef::Org.from_hash(Chef::JSONCompat.from_json(json)) + end + + class <<self + alias_method :json_create, :from_json + end + + def self.load(org_name) + response = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("organizations/#{org_name}") + Chef::Org.from_hash(response) + end + + def self.list(inflate=false) + orgs = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest('organizations') + if inflate + orgs.inject({}) do |org_map, (name, _url)| + org_map[name] = Chef::Org.load(name) + org_map + end + else + orgs + end + end + end +end diff --git a/spec/unit/org_spec.rb b/spec/unit/org_spec.rb new file mode 100644 index 0000000000..81d9bda020 --- /dev/null +++ b/spec/unit/org_spec.rb @@ -0,0 +1,196 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +require 'chef/org' +require 'tempfile' + +describe Chef::Org do + before(:each) do + @org = Chef::Org.new + end + + describe "initialize" do + it "should be a Chef::Org" do + expect(@org).to be_a_kind_of(Chef::Org) + end + end + + describe "name" do + it "should let you set the name to a string" do + @org.name "sg1" + expect(@org.name).to eq("sg1") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should not accept invalid characters" do + # capital letters + expect { @org.name "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @org.name "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @org.name "foo?" }.to raise_error(ArgumentError) + # & + expect { @org.name "foo&" }.to raise_error(ArgumentError) + # spaces + expect { @org.name "foo&" }.to raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @org.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "full_name" do + it "should let you set the full name" do + @org.full_name "foo" + expect(@org.full_name).to eq("foo") + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @org.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "private_key" do + it "should return the private key" do + @org.private_key("super private") + expect(@org.private_key).to eq("super private") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @org.private_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "when serializing to JSON" do + before(:each) do + @org.name("black") + @org.full_name("black crowes") + @json = @org.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) + end + + it "includes the full name value" do + expect(@json).to include(%q{"full_name":"black crowes"}) + end + + it "includes the private key when present" do + @org.private_key("monkeypants") + expect(@org.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).to_not include("private_key") + end + end + + describe "when deserializing from JSON" do + before(:each) do + org = { "name" => "turtle", + "full_name" => "turtle_club", + "private_key" => "pandas" } + @org = Chef::Org.from_json(org.to_json) + end + + it "deserializes to a Chef::Org object" do + expect(@org).to be_a_kind_of(Chef::Org) + end + + it "preserves the name" do + expect(@org.name).to eq("turtle") + end + + it "preserves the full_name" do + expect(@org.full_name).to eq("turtle_club") + end + + it "includes the private key if present" do + expect(@org.private_key).to eq("pandas") + end + end + + describe "API Interactions" do + before (:each) do + Chef::Config[:chef_server_root] = "http://www.example.com" + @rest = double('rest') + allow(Chef::REST).to receive(:new).and_return(@rest) + @org = Chef::Org.new + @org.name "foobar" + @org.full_name "foo bar bat" + end + + describe "list" do + before(:each) do + @response = {"foobar" => "http://www.example.com/organizations/foobar"} + @inflated_response = {"foobar" => @org } + allow(Chef::Org).to receive(:load).with("foobar").and_return(@org) + end + + it "lists all orgs" do + expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) + expect(Chef::Org.list).to eq(@response) + end + + it "inflate all orgs" do + expect(@rest).to receive(:get_rest).with("organizations").and_return(@response) + expect(Chef::Org.list(true)).to eq(@inflated_response) + end + end + + describe "create" do + it "creates a new org via the API" do + expect(@rest).to receive(:post_rest).with("organizations", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + @org.create + end + end + + describe "read" do + it "loads a named org from the API" do + expect(@rest).to receive(:get_rest).with("organizations/foobar").and_return({"name" => "foobar", "full_name" => "foo bar bat", "private_key" => "private"}) + org = Chef::Org.load("foobar") + expect(org.name).to eq("foobar") + expect(org.full_name).to eq("foo bar bat") + expect(org.private_key).to eq("private") + end + end + + describe "update" do + it "updates an existing org on via the API" do + expect(@rest).to receive(:put_rest).with("organizations/foobar", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + @org.update + end + end + + describe "destroy" do + it "deletes the specified org via the API" do + expect(@rest).to receive(:delete_rest).with("organizations/foobar") + @org.destroy + end + end + end +end |