summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteven Danna <steve@opscode.com>2014-11-19 13:49:27 +0000
committerLamont Granquist <lamont@scriptkiddie.org>2015-01-25 14:06:51 -0800
commit875b90d97a5c65efbda7853b4db25c18e808bd61 (patch)
tree51d8ea95290afde750d6b40e24cf254b3c5a2aa1
parentf923755a505c31745f6df14e201149128ba4ebec (diff)
downloadchef-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.rb143
-rw-r--r--spec/unit/org_spec.rb196
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